From f0e66adf8c1b90c00ae7b3dd124800fed35f371c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 7 Aug 2026 10:38:08 +0200 Subject: [PATCH 01/20] =?UTF-8?q?docs(wcag):=20specyfikacja=20fazy=202=20?= =?UTF-8?q?=E2=80=94=20skrot=20/=20oraz=20nawigacja=20po=20grafie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domyka dwa kryteria odroczone w iteracji z 2026-08-06 (2.1.4 skrot `/`, 2.5.7 przeciaganie grafu) plus 2.1.1 przy okazji, bo obsluga klawiatura kosztuje niewiele gdy funkcje nawigacyjne i tak powstaja dla przyciskow. Rozstrzygniecia: - 2.1.4 → mechanizm wylaczania w localStorage. Wzorce branzowe (GitHub: ustawienie konta; Gmail: dialog pod `?`) zakladaja zalogowanego uzytkownika z profilem, a audytowany zakres BPP to czesc publiczna, anonimowa — localStorage jest funkcjonalnym odpowiednikiem. Przelacznik w stopce, NIE na stronie deklaracji dostepnosci: ta jest konfigurowalna per uczelnia (pokazuj_deklaracje_dostepnosci 1/2/brak) i na czesci wdrozen nie istnieje albo prowadzi na serwer uczelni. - 2.5.7 → siedem przyciskow nawigacji (kierunki, zoom, dopasuj) nad wspolnym modulem czystych funkcji. Istniejaca wyszukiwarka wezlow NIE jest alternatywa: podswietla trafienia, ale nie przesuwa widoku. - 2.1.1 → te same funkcje pod strzalkami/+/-/Home przy focusie na kontenerze grafu. Klawisze +/- sa znakami drukowalnymi, wiec podlegaja 2.1.4 — spelniaja je trzecim wariantem (aktywne tylko przy focusie), wiec nie wymagaja przelacznika z czesci A. Nowe przyciski musza spelniac 2.5.8 (cel >=24x24 px) — naprawiajac 2.5.7 nie wolno zlamac sasiedniego kryterium. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01E8RjSZWMR6L7fuRDJJZW4L --- .../2026-08-07-wcag-skrot-i-graf-design.md | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md diff --git a/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md b/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md new file mode 100644 index 000000000..661f1a0a9 --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md @@ -0,0 +1,336 @@ +# Skrót `/` i nawigacja po grafie powiązań — WCAG 2.1.4, 2.5.7, 2.1.1 + +Data: 2026-08-07 +Poprzednik: `2026-08-06-wcag-naprawy-stwierdzone-design.md` +Gałąź: `fix-wcag-2140-2570`, stacked na `fix-wcag-naprawy-stwierdzone` (PR #732) + +## Cel + +Domknąć dwa kryteria WCAG 2.2 AA, które poprzednia iteracja **świadomie +odroczyła**, wpisując je do wykazu niezgodności: + +- **2.1.4 Character Key Shortcuts (A)** — skrót `/` wiązany na `document` +- **2.5.7 Dragging Movements (AA)** — nawigacja po grafie powiązań wyłącznie + przez przeciąganie + +Przy okazji grafu domykamy też **2.1.1 Keyboard (A)** — obsługa klawiaturą +kosztuje niewiele, gdy funkcje nawigacyjne i tak powstają dla przycisków. + +Odroczenie z 2026-08-06 miało uzasadnienie: brak nacisku regulacyjnego i brak +odbiorcy raportu. Ta iteracja jest decyzją, żeby mimo to poprawić dostępność +tam, gdzie koszt jest znany i ograniczony. + +## Zakres + +**Wchodzi:** + +- 2.1.4 — mechanizm wyłączania skrótów jednoznakowych (localStorage) +- 2.5.7 — przyciski nawigacji po grafie (kierunki, zoom, dopasowanie) +- 2.1.1 — obsługa grafu klawiaturą (strzałki, `+`/`−`, `Home`) +- 2.4.7 — widoczny pierścień focusa na kontenerze grafu +- usunięcie obu kryteriów z wykazu odroczonych w obu specyfikacjach + +**Nie wchodzi:** + +- 3.1.2 dla tytułu przełożonego (wymaga nowego pola w modelu) +- bramka CI z axe-core, skan szeroki, audyt ręczny, raport zgodności +- 2.5.8 (rozmiar celów dotykowych) w skali całego serwisu — ale **nowe** + przyciski muszą spełniać to kryterium, patrz „Część B" + +## Część A — 2.1.4, skrót `/` + +### Problem + +`src/django_bpp/templates/base.html:39-49` wiąże handler na `document`: + +```javascript +document.addEventListener('keydown', function(e) { + if (e.key === '/' && !$(e.target).is('input, textarea, select')) { + e.preventDefault(); + if (typeof openGlobalSearch === 'function') { + openGlobalSearch(null, true); + } + } +}); +``` + +Wykluczenie pól formularza nie wystarcza: gdy focus spoczywa na `body`, +linku albo przycisku, naciśnięcie `/` porywa klawisz. Kryterium wymaga +spełnienia **jednego z trzech** warunków — wyłączalności, przemapowania albo +aktywności wyłącznie przy focusie komponentu. Handler nie spełnia żadnego. + +Kryterium chroni przede wszystkim użytkowników sterowania głosem, których +wejściem są ciągi liter, oraz osoby z drżeniem rąk. + +### Wybrane wyjście: mechanizm wyłączania + +Wzorce branżowe potwierdzają ten kierunek. GitHub trzyma to jako ustawienie +konta („You can disable character key shortcuts, while still allowing +shortcuts that use modifier keys, in your accessibility settings"), Gmail +udostępnia dialog pod `?` z przełącznikiem. + +Obie realizacje zakładają jednak **zalogowanego użytkownika z profilem**, a +audytowany zakres BPP to część publiczna, anonimowa. Funkcjonalnym +odpowiednikiem jest `localStorage`: nie wymaga konta, przeżywa sesję, działa +per przeglądarka. Kryterium nie wymaga trwałości między urządzeniami — tylko +istnienia mechanizmu. + +Odrzucone alternatywy: + +- **zawężenie do focusa w polu wyszukiwania** — zgodne, ale skrót traci sens: + mając focus w wyszukiwarce wystarczy pisać; +- **modyfikator (`Alt+/`)** — wyprowadza skrót spoza kryterium jednym + ruchem, ale łamie konwencję branżową (`/` w GitHub, Gmail, Slack), a + `Alt+/` bywa zajęty przez czytniki ekranu; +- **całkowite usunięcie** — pełna zgodność zerowym kosztem, ale odbiera + działającą wygodę użytkownikom klawiatury; +- **przełącznik w obiekcie `Uczelnia`** — wyłączenie przez administratora + **nie spełnia** kryterium: WCAG wymaga mechanizmu dostępnego dla + użytkownika. + +### Komponenty + +**Moduł preferencji** — `src/bpp/static/bpp/js/skroty-klawiszowe.js`. +Jedna odpowiedzialność: odczyt i zapis preferencji. + +- klucz `localStorage`: `bpp.skrotyJednoznakowe`, wartości `"1"` / `"0"` +- `window.bppSkrotyWlaczone()` → `boolean`, **domyślnie `true`** (brak wpisu + = zachowanie niezmienione dla każdego, kto nic nie ustawił) +- `window.bppUstawSkroty(bool)` → zapisuje i zwraca nowy stan +- odporny na niedostępny `localStorage` (tryb prywatny, wyczerpany limit, + wyłączone ciasteczka): `try`/`catch` wokół obu operacji, w razie błędu + zwraca wartość domyślną zamiast rzucać wyjątkiem + +**Handler** w `base.html` — jeden dodatkowy warunek przed reakcją: + +```javascript +if (e.key === '/' && window.bppSkrotyWlaczone() + && !$(e.target).is('input, textarea, select')) { +``` + +Moduł ładowany przed inline'owym blokiem, tym samym wzorcem co +`related-records-highlight.js` w poprzedniej iteracji. + +**Przełącznik w stopce** — `src/django_bpp/templates/base_footer.html`. + +Przycisk (` | +``` + +Na końcu pliku dodaj podpięcie: + +```django + +``` + +- [ ] **Step 8: Dodaj styl przycisku** + +Przycisk ma wyglądać jak sąsiednie linki stopki, nie jak przycisk +formularza. W `src/bpp/static/scss/base_footer.scss` (bez podkreślnika — +tak nazywa się w tym projekcie, mimo że część partiali ma prefiks) dopisz: + +```scss +// Przelacznik skrotow klawiszowych (WCAG 2.1.4) ma wygladac jak sasiednie +// linki stopki — jest + + + + + + + +``` + +- [ ] **Step 5: Utwórz partial SCSS** + +Utwórz `src/bpp/static/scss/graf_powiazan.scss`: + +```scss +// Nakladka nawigacji po grafie powiazan (WCAG 2.5.7) oraz pierscien focusa +// kontenera grafu (2.4.7). Aplikacja powiazania_autorow nie ma wlasnego +// katalogu SCSS — graf.html opisuje wyglad atrybutami style="" — ale +// :focus-visible wymaga arkusza, wiec komponent siedzi w calosci tutaj. + +.graf-nawigacja { + position: absolute; + right: 10px; + bottom: 10px; + z-index: 1001; + + display: grid; + grid-template-columns: repeat(3, auto); + gap: 2px; + + padding: 6px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid #ddd; + border-radius: 4px; +} + +.graf-nawigacja__btn { + // WCAG 2.5.8 (Target Size Minimum): cel dotykowy nie mniejszy niz + // 24x24 px CSS. Naprawiajac 2.5.7 nie wolno zlamac sasiedniego + // kryterium. + min-width: 28px; + min-height: 28px; + + padding: 0; + margin: 0; + font-size: 15px; + line-height: 1; + + background: #fff; + border: 1px solid #ccc; + border-radius: 3px; + color: #333; + cursor: pointer; + + &:hover { + background: #f0f0f0; + } + + &:focus-visible { + outline: 2px solid #2c6cb0; + outline-offset: 1px; + } +} + +// WCAG 2.4.7: uzytkownik klawiatury musi widziec, ze graf przejal focus — +// inaczej nie wie, ze strzalki zaczely dzialac. +#cytoscape-container:focus-visible { + outline: 3px solid #2c6cb0; + outline-offset: -3px; +} +``` + +- [ ] **Step 6: Zaimportuj partial** + +W `src/bpp/static/scss/common.scss`, obok pozostałych importów (linie 7-18), +dopisz: + +```scss +@import "graf_powiazan"; +``` + +- [ ] **Step 7: Podepnij przyciski w `controls.js`** + +W +`src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js` +dopisz import na górze, obok istniejących: + +```javascript +import { przesun, zoomuj, dopasuj } from "./nawigacja.js"; +``` + +Na końcu funkcji `podepnijZdarzenia(ctx)`, przed zamykającym `}`, dopisz: + +```javascript + // --- nawigacja bez przeciagania (WCAG 2.5.7) --- + const KIERUNKI = { + "graf-nav-gora": "gora", + "graf-nav-dol": "dol", + "graf-nav-lewo": "lewo", + "graf-nav-prawo": "prawo" + }; + Object.keys(KIERUNKI).forEach(function (id) { + const btn = document.getElementById(id); + if (btn) { + btn.addEventListener("click", function () { + przesun(cy, KIERUNKI[id]); + }); + } + }); + + const btnZoomIn = document.getElementById("graf-nav-zoom-in"); + if (btnZoomIn) { + btnZoomIn.addEventListener("click", function () { + zoomuj(cy, 1.2); + }); + } + + const btnZoomOut = document.getElementById("graf-nav-zoom-out"); + if (btnZoomOut) { + btnZoomOut.addEventListener("click", function () { + zoomuj(cy, 1 / 1.2); + }); + } + + const btnDopasuj = document.getElementById("graf-nav-dopasuj"); + if (btnDopasuj) { + btnDopasuj.addEventListener("click", function () { + dopasuj(cy); + }); + } +``` + +- [ ] **Step 8: Przebuduj front i uruchom testy** + +```bash +grunt build +uv run pytest src/powiazania_autorow/tests/test_graf_dostepnosc.py -v +npx vitest run tests/js/nawigacja-grafu.test.js +``` + +Oczekiwane: 4 passed (pytest), 12 passed (vitest). + +- [ ] **Step 9: Commit** + +```bash +git add src/powiazania_autorow/templates/powiazania_autorow/graf.html \ + src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js \ + src/bpp/static/scss/graf_powiazan.scss \ + src/bpp/static/scss/common.scss \ + src/powiazania_autorow/tests/ +git commit -m "feat(wcag): przyciski nawigacji po grafie bez przeciagania (2.5.7)" +``` + +--- + +### Task 5: Obsługa grafu klawiaturą + +**Files:** +- Modify: `src/powiazania_autorow/templates/powiazania_autorow/graf.html:158-166` (atrybuty kontenera) +- Modify: `src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js` (handler `keydown`) +- Test: `src/powiazania_autorow/tests/test_graf_dostepnosc.py` (dopisanie) + +**Interfaces:** +- Consumes: `przesun`, `zoomuj`, `dopasuj` z Task 3 +- Produces: nic dla dalszych zadań + +- [ ] **Step 1: Dopisz testy** + +Do `src/powiazania_autorow/tests/test_graf_dostepnosc.py` dopisz: + +```python +def test_kontener_grafu_jest_fokusowalny(): + # Bez tabindex użytkownik klawiatury nigdy nie dotrze do grafu. + tresc = _tresc() + fragment = tresc.split('id="cytoscape-container"')[1][:600] + + assert 'tabindex="0"' in fragment + + +def test_kontener_grafu_ma_role_application(): + # Bez tego czytnik ekranu w trybie przeglądania sam obsłuży strzałki + # i nigdy nie dotrą one do grafu. + tresc = _tresc() + fragment = tresc.split('id="cytoscape-container"')[1][:600] + + assert 'role="application"' in fragment + + +def test_kontener_grafu_opisuje_dostepne_klawisze(): + # role="application" wycisza tryb przeglądania, więc aria-label jest + # jedynym sposobem, w jaki użytkownik pozna dostępne klawisze. + tresc = _tresc() + fragment = tresc.split('id="cytoscape-container"')[1][:600] + + assert "aria-label=" in fragment + for slowo in ("trzałk", "Home"): + assert slowo in fragment +``` + +- [ ] **Step 2: Uruchom testy — muszą paść** + +```bash +uv run pytest src/powiazania_autorow/tests/test_graf_dostepnosc.py -v +``` + +Oczekiwane: trzy nowe FAIL. + +- [ ] **Step 3: Dodaj atrybuty kontenera** + +W `src/powiazania_autorow/templates/powiazania_autorow/graf.html`, element +`#cytoscape-container` (linie 158-166), dopisz trzy atrybuty przed +`style=`: + +```django + tabindex="0" + role="application" + aria-label="Graf powiązań autorów. Strzałki przesuwają widok, plus i minus przybliżają, klawisz Home dopasowuje graf do ekranu." +``` + +- [ ] **Step 4: Dodaj handler klawiatury w `controls.js`** + +Na końcu funkcji `podepnijZdarzenia(ctx)`, po kodzie przycisków z Task 4, +dopisz: + +```javascript + // --- obsluga klawiatura (WCAG 2.1.1) --- + // Klawisze `+`/`-` sa znakami drukowalnymi, wiec podlegaja tez 2.1.4 — + // spelniaja je trzecim wariantem kryterium: dzialaja WYLACZNIE gdy + // kontener grafu ma focus, bo handler wisi na nim, nie na `document`. + const kontener = document.getElementById("cytoscape-container"); + if (kontener) { + kontener.addEventListener("keydown", function (e) { + let obsluzone = true; + + switch (e.key) { + case "ArrowUp": przesun(cy, "gora"); break; + case "ArrowDown": przesun(cy, "dol"); break; + case "ArrowLeft": przesun(cy, "lewo"); break; + case "ArrowRight": przesun(cy, "prawo"); break; + case "+": + case "=": zoomuj(cy, 1.2); break; + case "-": + case "_": zoomuj(cy, 1 / 1.2); break; + case "Home": dopasuj(cy); break; + default: obsluzone = false; + } + + // preventDefault WYLACZNIE dla obsluzonych klawiszy. Blokowanie + // wszystkiego zamknelo by Tab w grafie, czyli naprawiajac 2.1.1 + // stworzylibysmy pulapke klawiaturowa i zlamali 2.1.2. + if (obsluzone) { + e.preventDefault(); + } + }); + } +``` + +Warianty `=` i `_` są obok `+` i `-`, bo na większości układów klawiatury +`+` wymaga Shift. + +- [ ] **Step 5: Przebuduj front i uruchom testy** + +```bash +grunt build +uv run pytest src/powiazania_autorow/tests/test_graf_dostepnosc.py -v +``` + +Oczekiwane: 7 passed. + +- [ ] **Step 6: Commit** + +```bash +git add src/powiazania_autorow/templates/powiazania_autorow/graf.html \ + src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js \ + src/powiazania_autorow/tests/test_graf_dostepnosc.py +git commit -m "feat(wcag): obsluga grafu powiazan klawiatura (2.1.1, 2.4.7)" +``` + +--- + +### Task 6: Testy Playwright + +Testy szablonowe i jednostkowe nie dowodzą, że mechanizm **działa**. Bez +tego zadania usunięcie warunku z handlera nie wywaliłoby niczego. + +**Files:** +- Create: `src/integration_tests/test_wcag_skrot_i_graf.py` + +**Interfaces:** +- Consumes: `#bpp-przelacznik-skrotow` (Task 2), `#graf-nav-*` (Task 4), + `tabindex` na `#cytoscape-container` (Task 5) +- Produces: nic + +- [ ] **Step 1: Poznaj wzorzec z tego projektu** + +Sygnatura testu przeglądarkowego w BPP (z +`src/integration_tests/test_siec3d_bez_webgl.py:44-57`): + +```python +@pytest.mark.django_db(transaction=True) +def test_cos(channels_live_server, page: Page, transactional_db): + autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + url = reverse("bpp:browse_autor_powiazania", args=[autor.pk]) + page.goto(f"{channels_live_server.url}{url}", wait_until="domcontentloaded") +``` + +Trzy fixture: `channels_live_server`, `page: Page`, `transactional_db`. +Autora tworzy się przez `baker.make(Autor, …, pokazuj=True)` — nie ma +gotowej fixture autora dla tych testów. URL składa się przez f-string, bez +`+`. Import: `from playwright.sync_api import Page, expect`. + +**Wymaganie wstępne:** `make assets` — bez zbudowanego bundla strona nie ma +czego wykonać i testy padną. + +- [ ] **Step 2: Napisz testy** + +Utwórz `src/integration_tests/test_wcag_skrot_i_graf.py`: + +```python +"""WCAG 2.1.4 i 2.5.7/2.1.1 — testy zachowania w przeglądarce. + +Testy szablonowe dowodzą, że kod jest w pliku; te dowodzą, że działa. +Bez nich usunięcie warunku `bppSkrotyWlaczone()` z handlera albo +`preventDefault` z obsługi klawiatury nie wywaliłoby żadnego testu. +""" + +import pytest +from django.urls import reverse +from model_bakery import baker +from playwright.sync_api import Page, expect + +from bpp.models import Autor + + +def _url_autora(channels_live_server): + autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + return ( + f"{channels_live_server.url}" + f"{reverse('bpp:browse_autor', args=[autor.slug])}" + ) + + +@pytest.mark.django_db(transaction=True) +def test_skrot_otwiera_wyszukiwarke_domyslnie( + channels_live_server, page: Page, transactional_db +): + page.goto(_url_autora(channels_live_server), wait_until="domcontentloaded") + + page.keyboard.press("/") + + expect(page.locator("#globalSearchModal")).to_be_visible(timeout=5000) + + +@pytest.mark.django_db(transaction=True) +def test_wylaczenie_skrotu_dziala( + channels_live_server, page: Page, transactional_db +): + page.goto(_url_autora(channels_live_server), wait_until="domcontentloaded") + + page.locator("#bpp-przelacznik-skrotow").click() + page.keyboard.press("/") + page.wait_for_timeout(500) + + expect(page.locator("#globalSearchModal")).not_to_be_visible() + + +@pytest.mark.django_db(transaction=True) +def test_ponowne_wlaczenie_przywraca_skrot( + channels_live_server, page: Page, transactional_db +): + page.goto(_url_autora(channels_live_server), wait_until="domcontentloaded") + + przelacznik = page.locator("#bpp-przelacznik-skrotow") + przelacznik.click() + przelacznik.click() + page.keyboard.press("/") + + expect(page.locator("#globalSearchModal")).to_be_visible(timeout=5000) + + +@pytest.mark.django_db(transaction=True) +def test_przelacznik_aktualizuje_aria_pressed( + channels_live_server, page: Page, transactional_db +): + page.goto(_url_autora(channels_live_server), wait_until="domcontentloaded") + + przelacznik = page.locator("#bpp-przelacznik-skrotow") + expect(przelacznik).to_have_attribute("aria-pressed", "true") + + przelacznik.click() + expect(przelacznik).to_have_attribute("aria-pressed", "false") + + +@pytest.mark.django_db(transaction=True) +def test_preferencja_przezywa_przeladowanie( + channels_live_server, page: Page, transactional_db +): + url = _url_autora(channels_live_server) + page.goto(url, wait_until="domcontentloaded") + + page.locator("#bpp-przelacznik-skrotow").click() + page.reload(wait_until="domcontentloaded") + + expect(page.locator("#bpp-przelacznik-skrotow")).to_have_attribute( + "aria-pressed", "false" + ) +``` + +Widok strony autora został wybrany zamiast strony uczelni, bo nie wymaga +obiektu `Uczelnia` w bazie, a stopka i modal wyszukiwarki renderują się na +nim tak samo. `bpp:browse_autor` ma dwa warianty URL-a — po `pk` +(`urls.py:274`) i po `slug` (`urls.py:300`); `reverse` z argumentem `slug` +trafia w drugi. `Autor` tworzony przez `baker.make` dostaje slug +automatycznie (pole wyliczane przy zapisie). + +**Nie zamieniaj** testu zachowania na sprawdzenie obecności elementu w +HTML-u — testy szablonowe z Task 2 już to pokrywają, ten plik ma dowodzić +działania. + +- [ ] **Step 3: Uruchom testy** + +```bash +make assets +make playwright-install +uv run pytest src/integration_tests/test_wcag_skrot_i_graf.py -v +``` + +Pierwszy przebieg bywa wolny (zimny start testcontenerów) i `page.goto` +potrafi raz timeoutnąć — wtedy ponów. + +Jeżeli nazwa fixture live-servera albo widoku (`bpp:browse_uczelnia`) nie +pasuje, dostosuj ją do wzorca z kroku 1 — ale **nie** zamieniaj testu +zachowania na test obecności elementu w HTML-u. Testy szablonowe już to +pokrywają; ten plik ma dowodzić działania. + +Jeżeli strona uczelni nie renderuje przełącznika (np. wymaga obiektu +`Uczelnia` w bazie), utwórz go w teście przez `baker.make` albo użyj +istniejącej fixture — sprawdź `src/fixtures/conftest_*.py`. + +- [ ] **Step 4: Dopisz test grafu** + +Do tego samego pliku dopisz: + +```python +def _url_grafu(channels_live_server): + autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + return ( + f"{channels_live_server.url}" + f"{reverse('bpp:browse_autor_powiazania', args=[autor.pk])}" + ) + + +@pytest.mark.django_db(transaction=True) +def test_graf_ma_przyciski_nawigacji_i_jest_fokusowalny( + channels_live_server, page: Page, transactional_db +): + page.goto(_url_grafu(channels_live_server), wait_until="domcontentloaded") + + kontener = page.locator("#cytoscape-container") + expect(kontener).to_have_attribute("tabindex", "0") + expect(page.locator("#graf-nav-dopasuj")).to_be_visible(timeout=10000) + + page.locator("#graf-nav-dopasuj").click() + + +@pytest.mark.django_db(transaction=True) +def test_graf_nie_jest_pulapka_klawiaturowa( + channels_live_server, page: Page, transactional_db +): + # Handler robi preventDefault WYŁĄCZNIE dla obsłużonych klawiszy. Gdyby + # blokował wszystko, Tab przestałby wyprowadzać focus — czyli naprawiając + # 2.1.1 stworzylibyśmy pułapkę klawiaturową i złamali 2.1.2. + page.goto(_url_grafu(channels_live_server), wait_until="domcontentloaded") + + page.locator("#cytoscape-container").focus() + page.keyboard.press("Tab") + + assert page.evaluate("document.activeElement.id") != "cytoscape-container" +``` + +Widok `bpp:browse_autor_powiazania` przyjmuje `pk` — potwierdzone w +`test_siec3d_bez_webgl.py:67`, gdzie ten sam URL jest budowany przez +`reverse("bpp:browse_autor_powiazania", args=[autor.pk])`. + +Jeżeli graf nie renderuje się dla autora bez powiązań i przyciski nie +pojawią się w DOM, sprawdź warunek widoczności sieci +(`Autor.czy_pokazywac_siec_powiazan`) i ustaw wymagane pole przez +`baker.make`. Nakładka jest w szablonie statycznie, więc powinna być obecna +niezależnie od danych — jeśli tak nie jest, zgłoś to jako znalezisko, bo +oznaczałoby, że nawigacja znika akurat przy pustym grafie. + +- [ ] **Step 5: Uruchom całość i commit** + +```bash +uv run pytest src/integration_tests/test_wcag_skrot_i_graf.py -v +git add src/integration_tests/test_wcag_skrot_i_graf.py +git commit -m "test(wcag): testy przegladarkowe skrotu i nawigacji po grafie" +``` + +--- + +### Task 7: Wykaz odroczonych i newsfragment + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md` +- Modify: `docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md` +- Create: `src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst` + +**Interfaces:** +- Consumes: nic +- Produces: nic + +- [ ] **Step 1: Usuń 2.1.4 i 2.5.7 z wykazu w dokumencie 08-06** + +W sekcji „Wykaz odroczonych niezgodności" zastąp wpisy o 2.1.4 i 2.5.7 +jednym akapitem: + +```markdown +**2.1.4 i 2.5.7 — domknięte 2026-08-07.** +Oba kryteria zostały naprawione w kolejnej iteracji: skrót `/` dostał +mechanizm wyłączania (localStorage + przełącznik w stopce), graf powiązań — +przyciski nawigacji i obsługę klawiaturą. Szczegóły: +`2026-08-07-wcag-skrot-i-graf-design.md`. +``` + +- [ ] **Step 2: To samo w dokumencie 08-05** + +Ten sam akapit w sekcji „Odroczone niezgodności". + +- [ ] **Step 3: Przejrzyj oba dokumenty pod kątem pozostałych wzmianek** + +Poprzednia iteracja pokazała, że dokument o rozbudowanej historii zostawia +twierdzenia sprzeczne ze stanem faktycznym w miejscach, o których się nie +myśli — trzy kolejne przejścia znajdowały kolejne. + +```bash +grep -n "2\.1\.4\|2\.5\.7\|skrót\|skrot\|graf\|przeciąganie" \ + docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md \ + docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md +``` + +Dla KAŻDEGO trafienia rozstrzygnij, czy twierdzenie jest nadal prawdziwe. +Miejsca mówiące „odroczone", „niezgodne", „świadomie nie naprawiamy" dla +tych dwóch kryteriów wymagają dopisku +`**Korekta (2026-08-07):**` — zgodnie z konwencją przyjętą w tych +dokumentach, bez usuwania oryginalnego tekstu. + +W raporcie wymień **wszystkie** sprawdzone miejsca, także uznane za +niewymagające zmiany, z uzasadnieniem. + +- [ ] **Step 4: Napisz newsfragment** + +Utwórz `src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst`: + +```rst +Skrót klawiszowy ``/`` otwierający wyszukiwarkę można teraz wyłączyć — +przełącznik „Skróty klawiszowe" znajduje się w stopce strony. Ma to +znaczenie dla osób korzystających ze sterowania głosem, którym pojedyncze +znaki mimowolnie uruchamiały wyszukiwarkę (kryterium WCAG 2.2 AA 2.1.4). +Przy okazji naprawiono zdublowany obsługiwacz tego skrótu. + +Graf powiązań autorów da się teraz przesuwać i przybliżać przyciskami oraz +klawiaturą (strzałki, plus, minus, Home), a nie wyłącznie przeciąganiem +myszą — kryteria 2.5.7 i 2.1.1. +``` + +- [ ] **Step 5: Commit** + +```bash +git add docs/superpowers/specs/ src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst +git commit -m "docs(wcag): domkniecie 2.1.4 i 2.5.7 w wykazie, newsfragment" +``` + +--- + +### Task 8: Weryfikacja końcowa + +- [ ] **Step 1: Pełna suita Pythona** + +```bash +cd /Volumes/SSD/Programowanie/bpp-wcag-faza2 +make tests-without-playwright +``` + +Trwa do ~10 minut. Poczekaj w swojej turze, nie uruchamiaj w tle. + +- [ ] **Step 2: Testy JS** + +```bash +make js-tests +``` + +- [ ] **Step 3: Testy Playwright** + +```bash +make assets +make tests-only-playwright +``` + +- [ ] **Step 4: pre-commit** + +```bash +pre-commit +``` + +BEZ ARGUMENTÓW. Problemy naprawiaj ręcznie, edytorem, po jednym. + +- [ ] **Step 5: Spójność gałęzi** + +```bash +git log --oneline fix-wcag-naprawy-stwierdzone..HEAD +git diff --stat fix-wcag-naprawy-stwierdzone..HEAD +uv run python src/manage.py makemigrations --check --dry-run +``` + +Gałąź jest **stacked** na `fix-wcag-naprawy-stwierdzone`, więc bazą +porównania jest ta gałąź, nie `dev`. + +Dryf migracji w pakietach zewnętrznych (favicon, flexible_reports, siteblog) +jest zastany — zweryfikowano to w poprzedniej iteracji. Dryf w pakietach +`bpp`/`powiazania_autorow` byłby znaleziskiem: zgłoś, nie twórz migracji. + +- [ ] **Step 6: Obejrzyj efekt w przeglądarce** + +```bash +uv run run-site run --from-dump ~/db-backup-20260428-093811.pg_dump --no-browser +``` + +Sprawdź: przełącznik w stopce zmienia etykietę i faktycznie wyłącza skrót; +na stronie autora z włączoną siecią powiązań przyciski przesuwają graf, a +Tab dochodzi do kontenera i strzałki działają. diff --git a/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md b/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md index 03700776b..6aeac8ba1 100644 --- a/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md +++ b/docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md @@ -224,7 +224,7 @@ ekranu". **Podpięcie zdarzeń** w istniejącym `powiazania/controls.js`, zgodnie z tamtejszym wzorcem (`podepnijZdarzenia(ctx)`). -**Style** — nowy partial `src/bpp/static/scss/_graf_powiazan.scss`, +**Style** — nowy partial `src/bpp/static/scss/graf_powiazan.scss`, zaimportowany w `common.scss` obok pozostałych (`@import "graf_powiazan";`). Aplikacja `powiazania_autorow` **nie ma własnego katalogu SCSS** — `graf.html` From 306d0c919aa45e30466973f20db7e64675aacd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 7 Aug 2026 11:21:19 +0200 Subject: [PATCH 04/20] feat(wcag): modul preferencji skrotow jednoznakowych (2.1.4) --- src/bpp/static/bpp/js/skroty-klawiszowe.js | 65 +++++++++ tests/js/skroty-klawiszowe.test.js | 150 +++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/bpp/static/bpp/js/skroty-klawiszowe.js create mode 100644 tests/js/skroty-klawiszowe.test.js diff --git a/src/bpp/static/bpp/js/skroty-klawiszowe.js b/src/bpp/static/bpp/js/skroty-klawiszowe.js new file mode 100644 index 000000000..e1d8c6dd8 --- /dev/null +++ b/src/bpp/static/bpp/js/skroty-klawiszowe.js @@ -0,0 +1,65 @@ +/** + * Preferencja skrotow jednoznakowych (WCAG 2.1.4 Character Key Shortcuts). + * + * Kryterium wymaga, zeby skrot zlozony z samych znakow drukowalnych dalo sie + * wylaczyc, przemapowac albo ograniczyc do focusa komponentu. Skrot `/` + * otwierajacy wyszukiwarke globalna wisi na `document`, wiec wybieramy + * pierwsza droge. + * + * BPP w czesci publicznej nie ma kont uzytkownikow (GitHub i Gmail trzymaja + * taka preferencje w profilu), wiec zapisujemy ja w localStorage. Kryterium + * nie wymaga trwalosci miedzy urzadzeniami — tylko istnienia mechanizmu. + * + * Domyslka: WLACZONE. Brak wpisu nie moze zmieniac zachowania nikomu, kto + * nic nie ustawil. + */ +(function (window) { + "use strict"; + + var KLUCZ = "bpp.skrotyJednoznakowe"; + var ETYKIETA_WL = "Skróty klawiszowe: włączone"; + var ETYKIETA_WYL = "Skróty klawiszowe: wyłączone"; + + function bppSkrotyWlaczone() { + try { + return window.localStorage.getItem(KLUCZ) !== "0"; + } catch (e) { + // Tryb prywatny, wylaczone ciasteczka, wyczerpany limit — + // degradujemy do domyslki zamiast wywracac obsluge klawisza. + return true; + } + } + + function bppUstawSkroty(wlaczone) { + try { + window.localStorage.setItem(KLUCZ, wlaczone ? "1" : "0"); + } catch (e) { + // Zapis niemozliwy — preferencja nie przetrwa przeladowania, + // ale biezaca sesja i tak dziala na zwroconej wartosci. + } + return !!wlaczone; + } + + function bppPodepnijPrzelacznikSkrotow(el) { + if (!el) { + return; + } + + function odswiez() { + var wl = bppSkrotyWlaczone(); + el.setAttribute("aria-pressed", wl ? "true" : "false"); + el.textContent = wl ? ETYKIETA_WL : ETYKIETA_WYL; + } + + el.addEventListener("click", function () { + bppUstawSkroty(!bppSkrotyWlaczone()); + odswiez(); + }); + + odswiez(); + } + + window.bppSkrotyWlaczone = bppSkrotyWlaczone; + window.bppUstawSkroty = bppUstawSkroty; + window.bppPodepnijPrzelacznikSkrotow = bppPodepnijPrzelacznikSkrotow; +})(typeof window !== "undefined" ? window : this); diff --git a/tests/js/skroty-klawiszowe.test.js b/tests/js/skroty-klawiszowe.test.js new file mode 100644 index 000000000..fe02488e5 --- /dev/null +++ b/tests/js/skroty-klawiszowe.test.js @@ -0,0 +1,150 @@ +// @vitest-environment jsdom +// +// Preferencja skrotow jednoznakowych (WCAG 2.1.4). Kryterium wymaga, zeby +// uzytkownik mogl skrot wylaczyc — BPP w czesci publicznej nie ma kont, wiec +// preferencja siedzi w localStorage. Domyslka to "wlaczone": brak wpisu nie +// moze zmieniac zachowania nikomu, kto nic nie ustawil. + +import { describe, it, expect, beforeEach } from "vitest"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +const ZRODLO = readFileSync( + resolve(__dirname, "../../src/bpp/static/bpp/js/skroty-klawiszowe.js"), + "utf-8" +); + +function zaladuj() { + // Modul jest skryptem window-globalnym (IIFE), nie modulem ESM — + // wykonujemy go na biezacym window jsdom. + new Function(ZRODLO).call(window); + return window; +} + +beforeEach(() => { + // Polyfill localStorage jeśli nie jest dostępny (jsdom 25+) + if (!window.localStorage) { + const store = {}; + window.localStorage = { + getItem: (key) => store[key] || null, + setItem: (key, value) => { + store[key] = String(value); + }, + removeItem: (key) => { + delete store[key]; + }, + clear: () => { + Object.keys(store).forEach((key) => delete store[key]); + }, + key: (index) => Object.keys(store)[index] || null, + get length() { + return Object.keys(store).length; + }, + }; + } + window.localStorage.clear(); + delete window.bppSkrotyWlaczone; + delete window.bppUstawSkroty; + delete window.bppPodepnijPrzelacznikSkrotow; +}); + +describe("bppSkrotyWlaczone", () => { + it("domyslnie wlaczone gdy brak wpisu", () => { + expect(zaladuj().bppSkrotyWlaczone()).toBe(true); + }); + + it('"0" wylacza', () => { + window.localStorage.setItem("bpp.skrotyJednoznakowe", "0"); + expect(zaladuj().bppSkrotyWlaczone()).toBe(false); + }); + + it('"1" wlacza', () => { + window.localStorage.setItem("bpp.skrotyJednoznakowe", "1"); + expect(zaladuj().bppSkrotyWlaczone()).toBe(true); + }); + + it("smiec w localStorage traktowany jak brak wpisu", () => { + window.localStorage.setItem("bpp.skrotyJednoznakowe", "tak"); + expect(zaladuj().bppSkrotyWlaczone()).toBe(true); + }); + + it("nie rzuca gdy localStorage niedostepny", () => { + const oryginalny = window.localStorage.getItem; + window.localStorage.getItem = () => { + throw new Error("SecurityError"); + }; + const w = zaladuj(); + expect(w.bppSkrotyWlaczone()).toBe(true); + window.localStorage.getItem = oryginalny; + }); +}); + +describe("bppUstawSkroty", () => { + it("zapisuje wylaczenie i zwraca nowy stan", () => { + const w = zaladuj(); + expect(w.bppUstawSkroty(false)).toBe(false); + expect(window.localStorage.getItem("bpp.skrotyJednoznakowe")).toBe("0"); + expect(w.bppSkrotyWlaczone()).toBe(false); + }); + + it("zapisuje wlaczenie", () => { + const w = zaladuj(); + w.bppUstawSkroty(false); + expect(w.bppUstawSkroty(true)).toBe(true); + expect(window.localStorage.getItem("bpp.skrotyJednoznakowe")).toBe("1"); + }); + + it("nie rzuca gdy zapis niemozliwy", () => { + const oryginalny = window.localStorage.setItem; + window.localStorage.setItem = () => { + throw new Error("QuotaExceededError"); + }; + const w = zaladuj(); + expect(() => w.bppUstawSkroty(false)).not.toThrow(); + window.localStorage.setItem = oryginalny; + }); +}); + +describe("bppPodepnijPrzelacznikSkrotow", () => { + function przycisk() { + const el = window.document.createElement("button"); + window.document.body.appendChild(el); + return el; + } + + it("ustawia stan poczatkowy na wlaczony", () => { + const w = zaladuj(); + const el = przycisk(); + w.bppPodepnijPrzelacznikSkrotow(el); + expect(el.getAttribute("aria-pressed")).toBe("true"); + expect(el.textContent).toContain("włączone"); + }); + + it("klik wylacza skroty i aktualizuje etykiete", () => { + const w = zaladuj(); + const el = przycisk(); + w.bppPodepnijPrzelacznikSkrotow(el); + el.click(); + expect(w.bppSkrotyWlaczone()).toBe(false); + expect(el.getAttribute("aria-pressed")).toBe("false"); + expect(el.textContent).toContain("wyłączone"); + }); + + it("drugi klik wraca do stanu wyjsciowego", () => { + const w = zaladuj(); + const el = przycisk(); + w.bppPodepnijPrzelacznikSkrotow(el); + el.click(); + el.click(); + expect(w.bppSkrotyWlaczone()).toBe(true); + expect(el.getAttribute("aria-pressed")).toBe("true"); + }); + + it("odzwierciedla stan zapisany wczesniej", () => { + window.localStorage.setItem("bpp.skrotyJednoznakowe", "0"); + const w = zaladuj(); + const el = przycisk(); + w.bppPodepnijPrzelacznikSkrotow(el); + expect(el.getAttribute("aria-pressed")).toBe("false"); + }); +}); From 8087bfc7574b32fb54d3a68512f9f60e4d90e316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 7 Aug 2026 11:32:37 +0200 Subject: [PATCH 05/20] =?UTF-8?q?fix(wcag):=20fallback=20w=20pami=C4=99ci?= =?UTF-8?q?=20dla=20skr=C3=B3t=C3=B3w=20w=20trybie=20prywatnym=20+=202=20t?= =?UTF-8?q?esty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bpp/static/bpp/js/skroty-klawiszowe.js | 15 ++++++--- tests/js/skroty-klawiszowe.test.js | 36 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/bpp/static/bpp/js/skroty-klawiszowe.js b/src/bpp/static/bpp/js/skroty-klawiszowe.js index e1d8c6dd8..214755471 100644 --- a/src/bpp/static/bpp/js/skroty-klawiszowe.js +++ b/src/bpp/static/bpp/js/skroty-klawiszowe.js @@ -19,23 +19,30 @@ var KLUCZ = "bpp.skrotyJednoznakowe"; var ETYKIETA_WL = "Skróty klawiszowe: włączone"; var ETYKIETA_WYL = "Skróty klawiszowe: wyłączone"; + var stanAwaryjny = null; // Fallback w pamięci gdy localStorage niedostępny function bppSkrotyWlaczone() { + if (stanAwaryjny !== null) { + return stanAwaryjny; + } try { return window.localStorage.getItem(KLUCZ) !== "0"; } catch (e) { // Tryb prywatny, wylaczone ciasteczka, wyczerpany limit — - // degradujemy do domyslki zamiast wywracac obsluge klawisza. - return true; + // czytamy fallback z pamięci sesji. + return stanAwaryjny !== null ? stanAwaryjny : true; } } function bppUstawSkroty(wlaczone) { try { window.localStorage.setItem(KLUCZ, wlaczone ? "1" : "0"); + stanAwaryjny = null; // Zapis się powiedział — czyść fallback } catch (e) { - // Zapis niemozliwy — preferencja nie przetrwa przeladowania, - // ale biezaca sesja i tak dziala na zwroconej wartosci. + // Zapis niemozliwy (tryb prywatny, limit, itp.) — trzymaj + // stan w pamięci sesji. Przełącznik będzie działa w bieżącej + // sesji, choć preferencja nie przetrwa przeładowania. + stanAwaryjny = !!wlaczone; } return !!wlaczone; } diff --git a/tests/js/skroty-klawiszowe.test.js b/tests/js/skroty-klawiszowe.test.js index fe02488e5..a9819bd5a 100644 --- a/tests/js/skroty-klawiszowe.test.js +++ b/tests/js/skroty-klawiszowe.test.js @@ -103,6 +103,42 @@ describe("bppUstawSkroty", () => { expect(() => w.bppUstawSkroty(false)).not.toThrow(); window.localStorage.setItem = oryginalny; }); + + it("fallback: gdy setItem rzuca, stan trzyma się w sesji (nie w localStorage)", () => { + const oryginalny = window.localStorage.setItem; + window.localStorage.setItem = () => { + throw new Error("QuotaExceededError"); + }; + const w = zaladuj(); + w.bppUstawSkroty(false); + // localStorage jest puste (zapis się nie powiódł) + expect(window.localStorage.getItem("bpp.skrotyJednoznakowe")).toBe(null); + // ale bppSkrotyWlaczone() zwraca false (z fallback-u w pamięci) + expect(w.bppSkrotyWlaczone()).toBe(false); + window.localStorage.setItem = oryginalny; + }); + + it("fallback: przełącznik działa w sesji nawet bez localStorage", () => { + const oryginalny = window.localStorage.setItem; + window.localStorage.setItem = () => { + throw new Error("QuotaExceededError"); + }; + const w = zaladuj(); + const el = window.document.createElement("button"); + window.document.body.appendChild(el); + w.bppPodepnijPrzelacznikSkrotow(el); + // Initialnie włączone + expect(el.getAttribute("aria-pressed")).toBe("true"); + // Klik wyłącza + el.click(); + expect(el.getAttribute("aria-pressed")).toBe("false"); + expect(el.textContent).toContain("wyłączone"); + // Drugi klik włącza + el.click(); + expect(el.getAttribute("aria-pressed")).toBe("true"); + expect(el.textContent).toContain("włączone"); + window.localStorage.setItem = oryginalny; + }); }); describe("bppPodepnijPrzelacznikSkrotow", () => { From e2e33094532c50631ef383cfac78eca351a0023b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 7 Aug 2026 11:49:50 +0200 Subject: [PATCH 06/20] feat(wcag): przelacznik skrotow w stopce, likwidacja duplikatu handlera (2.1.4) Co-Authored-By: Claude Opus 5 (1M context) --- src/bpp/static/scss/base_footer.scss | 13 +++++ src/bpp/templates/browse/uczelnia.html | 10 +++- .../tests/test_wcag/test_skrot_szukania.py | 54 +++++++++++++++++++ src/django_bpp/templates/base.html | 19 ++----- src/django_bpp/templates/base_footer.html | 15 ++++++ .../templates/global_search_modal.html | 8 ++- 6 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 src/bpp/tests/test_wcag/test_skrot_szukania.py diff --git a/src/bpp/static/scss/base_footer.scss b/src/bpp/static/scss/base_footer.scss index f1a4d9000..aec49004e 100644 --- a/src/bpp/static/scss/base_footer.scss +++ b/src/bpp/static/scss/base_footer.scss @@ -48,3 +48,16 @@ padding: 40px 0; z-index: 10; } + +// Przelacznik skrotow klawiszowych (WCAG 2.1.4) ma wygladac jak sasiednie +// linki stopki — jest + + + + + + + diff --git a/src/powiazania_autorow/tests/__init__.py b/src/powiazania_autorow/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/powiazania_autorow/tests/test_graf_dostepnosc.py b/src/powiazania_autorow/tests/test_graf_dostepnosc.py new file mode 100644 index 000000000..05af5f8c9 --- /dev/null +++ b/src/powiazania_autorow/tests/test_graf_dostepnosc.py @@ -0,0 +1,63 @@ +"""Dostępność grafu powiązań: nawigacja wskaźnikiem (2.5.7) i klawiaturą (2.1.1). + +Graf stoi na Cytoscape.js i do tej pory dawał się przesuwać wyłącznie +przeciąganiem. Kryterium 2.5.7 wymaga alternatywy realizowanej pojedynczym +wskaźnikiem — stąd przyciski. Sam kontener zyskuje `tabindex`, żeby te same +funkcje dało się wywołać z klawiatury (2.1.1). +""" + +from pathlib import Path + +SZABLON = ( + Path(__file__).resolve().parents[1] + / "templates" + / "powiazania_autorow" + / "graf.html" +) + +PRZYCISKI = [ + "graf-nav-gora", + "graf-nav-dol", + "graf-nav-lewo", + "graf-nav-prawo", + "graf-nav-zoom-in", + "graf-nav-zoom-out", + "graf-nav-dopasuj", +] + + +def _tresc(): + return SZABLON.read_text(encoding="utf-8") + + +def test_wszystkie_przyciski_nawigacji_obecne(): + tresc = _tresc() + for identyfikator in PRZYCISKI: + assert f'id="{identyfikator}"' in tresc, f"brak przycisku {identyfikator}" + + +def test_kazdy_przycisk_ma_aria_label(): + # Sam glif strzałki nic nie mówi czytnikowi ekranu. + tresc = _tresc() + fragmenty = tresc.split(" Date: Fri, 7 Aug 2026 12:26:37 +0200 Subject: [PATCH 09/20] feat(wcag): obsluga grafu powiazan klawiatura (2.1.1, 2.4.7) Co-Authored-By: Claude Opus 5 (1M context) --- .../js/powiazania/controls.js | 31 +++++++++++++++++++ .../templates/powiazania_autorow/graf.html | 3 ++ .../tests/test_graf_dostepnosc.py | 28 +++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js index 2db0c185c..24f2db098 100644 --- a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js +++ b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js @@ -409,4 +409,35 @@ export function podepnijZdarzenia(ctx) { dopasuj(cy); }); } + + // --- obsluga klawiatura (WCAG 2.1.1) --- + // Klawisze `+`/`-` sa znakami drukowalnymi, wiec podlegaja tez 2.1.4 — + // spelniaja je trzecim wariantem kryterium: dzialaja WYLACZNIE gdy + // kontener grafu ma focus, bo handler wisi na nim, nie na `document`. + const kontener = document.getElementById("cytoscape-container"); + if (kontener) { + kontener.addEventListener("keydown", function (e) { + let obsluzone = true; + + switch (e.key) { + case "ArrowUp": przesun(cy, "gora"); break; + case "ArrowDown": przesun(cy, "dol"); break; + case "ArrowLeft": przesun(cy, "lewo"); break; + case "ArrowRight": przesun(cy, "prawo"); break; + case "+": + case "=": zoomuj(cy, 1.2); break; + case "-": + case "_": zoomuj(cy, 1 / 1.2); break; + case "Home": dopasuj(cy); break; + default: obsluzone = false; + } + + // preventDefault WYLACZNIE dla obsluzonych klawiszy. Blokowanie + // wszystkiego zamknelo by Tab w grafie, czyli naprawiajac 2.1.1 + // stworzylibysmy pulapke klawiaturowa i zlamali 2.1.2. + if (obsluzone) { + e.preventDefault(); + } + }); + } } diff --git a/src/powiazania_autorow/templates/powiazania_autorow/graf.html b/src/powiazania_autorow/templates/powiazania_autorow/graf.html index aca86e1f6..a7cf7b6d6 100644 --- a/src/powiazania_autorow/templates/powiazania_autorow/graf.html +++ b/src/powiazania_autorow/templates/powiazania_autorow/graf.html @@ -156,6 +156,9 @@

Sieć powiązań — {{ autor }}

Date: Fri, 7 Aug 2026 12:40:30 +0200 Subject: [PATCH 10/20] fix(wcag): nie przechwytuj skrotow przegladarki w grafie + testy JS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handler klawiatury w grafie powiazan (Task 5) przechwytywal takze Ctrl/Cmd+Plus/Minus (zoom przegladarki, WCAG 1.4.4), Ctrl+Home (przewijanie na gore) i Alt+strzalka (nawigacja wstecz). Naprawiajac 2.1.1 lamalismy funkcje wazniejsza dla tej samej grupy uzytkownikow. Logike mapowania klawisza na akcje wydzielono z controls.js do nawigacja.js jako obsluzKlawisz(cy, e) — wczesne wyjscie na ctrlKey/metaKey/altKey (shiftKey celowo pominiety, bo + czesto wymaga Shift), zwraca true tylko dla obsluzonych klawiszy. controls.js redukuje sie do wywolania i warunkowego preventDefault. Dodano tests/js/graf-klawiatura.test.js (18 testow, atrapa cy z nawigacja-grafu.test.js) pokrywajacy strzalki, +/-/=/_, Home, klawisze nieobslugiwane (Tab/Escape/Enter/litera) oraz skroty z modyfikatorami. Co-Authored-By: Claude Opus 5 (1M context) --- .../js/powiazania/controls.js | 21 +- .../js/powiazania/nawigacja.js | 47 +++++ tests/js/graf-klawiatura.test.js | 181 ++++++++++++++++++ 3 files changed, 232 insertions(+), 17 deletions(-) create mode 100644 tests/js/graf-klawiatura.test.js diff --git a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js index 24f2db098..bb2ca1da2 100644 --- a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js +++ b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js @@ -3,7 +3,7 @@ // źródeł/wydawców, opcje zaawansowane, metryka, układ, wyszukiwarka, // odśwież oraz eksport PNG/SVG. Debounce dla żądań sterowanych suwakami. import { pobierzPlik } from "./dom.js"; -import { przesun, zoomuj, dopasuj } from "./nawigacja.js"; +import { przesun, zoomuj, dopasuj, obsluzKlawisz } from "./nawigacja.js"; import { pokazPanelAutora, pokazTooltipAutor, @@ -411,31 +411,18 @@ export function podepnijZdarzenia(ctx) { } // --- obsluga klawiatura (WCAG 2.1.1) --- + // Mapowanie klawisz -> akcja siedzi w obsluzKlawisz (nawigacja.js), + // zeby dalo sie je przetestowac bez importowania calego controls.js. // Klawisze `+`/`-` sa znakami drukowalnymi, wiec podlegaja tez 2.1.4 — // spelniaja je trzecim wariantem kryterium: dzialaja WYLACZNIE gdy // kontener grafu ma focus, bo handler wisi na nim, nie na `document`. const kontener = document.getElementById("cytoscape-container"); if (kontener) { kontener.addEventListener("keydown", function (e) { - let obsluzone = true; - - switch (e.key) { - case "ArrowUp": przesun(cy, "gora"); break; - case "ArrowDown": przesun(cy, "dol"); break; - case "ArrowLeft": przesun(cy, "lewo"); break; - case "ArrowRight": przesun(cy, "prawo"); break; - case "+": - case "=": zoomuj(cy, 1.2); break; - case "-": - case "_": zoomuj(cy, 1 / 1.2); break; - case "Home": dopasuj(cy); break; - default: obsluzone = false; - } - // preventDefault WYLACZNIE dla obsluzonych klawiszy. Blokowanie // wszystkiego zamknelo by Tab w grafie, czyli naprawiajac 2.1.1 // stworzylibysmy pulapke klawiaturowa i zlamali 2.1.2. - if (obsluzone) { + if (obsluzKlawisz(cy, e)) { e.preventDefault(); } }); diff --git a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js index 8a28f7ce4..eb29346e1 100644 --- a/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js +++ b/src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js @@ -52,3 +52,50 @@ export function zoomuj(cy, wspolczynnik) { export function dopasuj(cy) { cy.fit(); } + +// Mapuje zdarzenie keydown na akcje nawigacji po grafie (WCAG 2.1.1). +// Zwraca true, jesli klawisz zostal obsluzony — wolajacy uzywa tego, zeby +// zdecydowac, czy wywolac preventDefault (WYLACZNIE dla obsluzonych +// klawiszy; inaczej Tab zostalby zablokowany w grafie — pulapka +// klawiaturowa, zlamanie 2.1.2). +// +// Skroty z modyfikatorem naleza do przegladarki, nie do nas: Ctrl/Cmd +/- +// to zoom strony (WCAG 1.4.4 Resize Text), Alt+strzalka to nawigacja +// wstecz/wprzod, Ctrl+Home to przewijanie na gore. Przechwycenie ich +// zlamaloby funkcje wazniejsze niz nawigacja po grafie — stad wczesne +// wyjscie. `shiftKey` NIE wchodzi do tego warunku: na wielu ukladach +// klawiatury `+` wymaga Shift, wiec jego zablokowanie zepsuloby +// przyblizanie grafu tej samej klawiszologii, ktora ma dzialac. +export function obsluzKlawisz(cy, e) { + if (e.ctrlKey || e.metaKey || e.altKey) { + return false; + } + + switch (e.key) { + case "ArrowUp": + przesun(cy, "gora"); + return true; + case "ArrowDown": + przesun(cy, "dol"); + return true; + case "ArrowLeft": + przesun(cy, "lewo"); + return true; + case "ArrowRight": + przesun(cy, "prawo"); + return true; + case "+": + case "=": + zoomuj(cy, 1.2); + return true; + case "-": + case "_": + zoomuj(cy, 1 / 1.2); + return true; + case "Home": + dopasuj(cy); + return true; + default: + return false; + } +} diff --git a/tests/js/graf-klawiatura.test.js b/tests/js/graf-klawiatura.test.js new file mode 100644 index 000000000..69969cb12 --- /dev/null +++ b/tests/js/graf-klawiatura.test.js @@ -0,0 +1,181 @@ +// Obsluga grafu powiazan klawiatura (WCAG 2.1.1) — mapowanie klawisza na +// akcje (obsluzKlawisz) siedzi w nawigacja.js, wiec testujemy je na atrapie +// cy (jak w nawigacja-grafu.test.js), bez importowania controls.js (ktory +// ciagnie pol aplikacji). + +import { describe, it, expect, beforeEach } from "vitest"; +import { obsluzKlawisz } from "../../src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js"; + +function atrapaCy(opcje) { + opcje = opcje || {}; + const stan = { + panBy: null, + zoomArg: null, + fitWolane: false, + poziomZoom: opcje.zoom === undefined ? 1 : opcje.zoom + }; + return { + _stan: stan, + width: () => (opcje.width === undefined ? 1000 : opcje.width), + height: () => (opcje.height === undefined ? 500 : opcje.height), + minZoom: () => 0.1, + maxZoom: () => 4, + zoom: function (arg) { + if (arg === undefined) { + return stan.poziomZoom; + } + stan.zoomArg = arg; + stan.poziomZoom = arg.level; + return undefined; + }, + panBy: function (arg) { + stan.panBy = arg; + }, + fit: function () { + stan.fitWolane = true; + } + }; +} + +// Zdarzenie klawiatury jako zwykly obiekt — obsluzKlawisz nie wywoluje na +// nim nic poza odczytem pol, wiec nie potrzeba prawdziwego KeyboardEvent. +function zdarzenie(key, modyfikatory) { + modyfikatory = modyfikatory || {}; + return { + key: key, + ctrlKey: modyfikatory.ctrlKey || false, + metaKey: modyfikatory.metaKey || false, + altKey: modyfikatory.altKey || false, + shiftKey: modyfikatory.shiftKey || false + }; +} + +describe("obsluzKlawisz — strzalki", () => { + let cy; + beforeEach(() => { + cy = atrapaCy(); + }); + + it("ArrowUp przesuwa w gore i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowUp")); + expect(wynik).toBe(true); + expect(cy._stan.panBy.y).toBeGreaterThan(0); + expect(cy._stan.panBy.x).toBe(0); + }); + + it("ArrowDown przesuwa w dol i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowDown")); + expect(wynik).toBe(true); + expect(cy._stan.panBy.y).toBeLessThan(0); + expect(cy._stan.panBy.x).toBe(0); + }); + + it("ArrowLeft przesuwa w lewo i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowLeft")); + expect(wynik).toBe(true); + expect(cy._stan.panBy.x).toBeGreaterThan(0); + expect(cy._stan.panBy.y).toBe(0); + }); + + it("ArrowRight przesuwa w prawo i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowRight")); + expect(wynik).toBe(true); + expect(cy._stan.panBy.x).toBeLessThan(0); + expect(cy._stan.panBy.y).toBe(0); + }); +}); + +describe("obsluzKlawisz — zoom", () => { + let cy; + beforeEach(() => { + cy = atrapaCy({ zoom: 1 }); + }); + + it("'+' przyblizA (wspolczynnik > 1) i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("+")); + expect(wynik).toBe(true); + expect(cy._stan.zoomArg.level).toBeGreaterThan(1); + }); + + it("'=' przyblizA i zwraca true (bez Shift na wiekszosci ukladow)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("=")); + expect(wynik).toBe(true); + expect(cy._stan.zoomArg.level).toBeGreaterThan(1); + }); + + it("'-' oddala (wspolczynnik < 1) i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("-")); + expect(wynik).toBe(true); + expect(cy._stan.zoomArg.level).toBeLessThan(1); + }); + + it("'_' oddala i zwraca true", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("_")); + expect(wynik).toBe(true); + expect(cy._stan.zoomArg.level).toBeLessThan(1); + }); +}); + +describe("obsluzKlawisz — Home", () => { + it("Home dopasowuje graf (cy.fit()) i zwraca true", () => { + const cy = atrapaCy(); + const wynik = obsluzKlawisz(cy, zdarzenie("Home")); + expect(wynik).toBe(true); + expect(cy._stan.fitWolane).toBe(true); + }); +}); + +describe("obsluzKlawisz — klawisze nieobslugiwane (bez pulapki, 2.1.2)", () => { + let cy; + beforeEach(() => { + cy = atrapaCy(); + }); + + it.each(["Tab", "Escape", "Enter", "a"])( + "%s zwraca false i nic nie wola", + (klawisz) => { + const wynik = obsluzKlawisz(cy, zdarzenie(klawisz)); + expect(wynik).toBe(false); + expect(cy._stan.panBy).toBeNull(); + expect(cy._stan.zoomArg).toBeNull(); + expect(cy._stan.fitWolane).toBe(false); + } + ); +}); + +describe("obsluzKlawisz — skroty przegladarki nie sa przechwytywane (1.4.4)", () => { + let cy; + beforeEach(() => { + cy = atrapaCy(); + }); + + it("Ctrl+ArrowUp zwraca false i nic nie wola (skrot przegladarki)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowUp", { ctrlKey: true })); + expect(wynik).toBe(false); + expect(cy._stan.panBy).toBeNull(); + }); + + it("Cmd(meta)+'+' zwraca false (Cmd+Plus to zoom przegladarki)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("+", { metaKey: true })); + expect(wynik).toBe(false); + expect(cy._stan.zoomArg).toBeNull(); + }); + + it("Ctrl+Home zwraca false (przewijanie strony na gore)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("Home", { ctrlKey: true })); + expect(wynik).toBe(false); + expect(cy._stan.fitWolane).toBe(false); + }); + + it("Alt+ArrowLeft zwraca false (nawigacja wstecz przegladarki)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("ArrowLeft", { altKey: true })); + expect(wynik).toBe(false); + expect(cy._stan.panBy).toBeNull(); + }); + + it("'+' z Shift zwraca true i przyblizA (Shift potrzebny do wpisania +)", () => { + const wynik = obsluzKlawisz(cy, zdarzenie("+", { shiftKey: true })); + expect(wynik).toBe(true); + expect(cy._stan.zoomArg.level).toBeGreaterThan(1); + }); +}); From f2144ba3e8aeb92489129b88101cd58325d5bc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 7 Aug 2026 13:37:13 +0200 Subject: [PATCH 11/20] test(wcag): testy przegladarkowe skrotu / oraz nawigacji po grafie Testy szablonowe z zadan 1-5 dowodza tylko, ze kod jest w plikach -- usuniecie warunku bppSkrotyWlaczone() z handlera skrotu / albo preventDefault z obslugi klawiatury grafu nie wywalaloby zadnego testu. Ten plik dowodzi zachowania w prawdziwej przegladarce (Playwright + channels_live_server). 10 testow: - 5x WCAG 2.1.4 (skrot / i jego wylacznik w stopce): domyslne otwarcie wyszukiwarki, wylaczenie dziala, ponowne wlaczenie przywraca skrot, aria-pressed sie aktualizuje, preferencja przezywa przeladowanie (localStorage). - 5x WCAG 2.5.7/2.1.1 (nawigacja po grafie powiazan): przyciski widoczne + kontener fokusowalny, klik zoom-in realnie zmienia cy.zoom(), klik kierunkowy realnie zmienia cy.pan(), strzalka z klawiatury realnie zmienia cy.pan(), Tab nie jest zlapany w pulapke klawiaturowa (2.1.2). Testy zoom/pan czytaja stan z container._cyreg.cy (wewnetrzny rejestr Cytoscape.js) PRZED i PO interakcji, zeby padaly gdy handler klikniecia zniknie -- sama widocznosc przycisku o tym nie swiadczy. Self-review (potwierdzone recznie, patrz raport): usuniecie warunku bppSkrotyWlaczone(), wylaczenie obsluzKlawisz() oraz odpiecie handlerow klikniec przyciskow kierunkowych/zoom kazde osobno wywalaly odpowiednie testy na czerwono. Co-Authored-By: Claude Opus 5 (1M context) --- .../test_wcag_skrot_i_graf.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 src/integration_tests/test_wcag_skrot_i_graf.py diff --git a/src/integration_tests/test_wcag_skrot_i_graf.py b/src/integration_tests/test_wcag_skrot_i_graf.py new file mode 100644 index 000000000..6eb9c131f --- /dev/null +++ b/src/integration_tests/test_wcag_skrot_i_graf.py @@ -0,0 +1,253 @@ +"""WCAG 2.1.4 i 2.5.7/2.1.1 — testy zachowania w przeglądarce. + +Testy szablonowe (``src/bpp/tests/test_wcag/``) dowodzą, że kod jest w +pliku; te dowodzą, że działa. Bez nich usunięcie warunku +``bppSkrotyWlaczone()`` z handlera skrótu ``/``, albo ``preventDefault`` +z obsługi klawiatury grafu, albo samych handlerów kliknięcia przycisków +nawigacji — nie wywaliłoby żadnego testu. + +Widok strony autora (``bpp:browse_autor``) wybrany zamiast strony uczelni, +bo nie wymaga obiektu ``Uczelnia`` w bazie (patrz też +``test_siec3d_bez_webgl.py``, który idzie tą samą drogą), a stopka i modal +wyszukiwarki renderują się na nim tak samo. + +WYMAGANIE WSTĘPNE: ``make assets`` — bez zbudowanego bundla strona nie ma +czego wykonać i testy padną na braku elementów / błędnym zachowaniu JS. +""" + +import pytest +from django.urls import reverse +from model_bakery import baker +from playwright.sync_api import Page, expect + +from bpp.models import Autor + + +def _url_autora(channels_live_server): + autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + return ( + f"{channels_live_server.url}" + f"{reverse('bpp:browse_autor', args=[autor.slug])}" + ) + + +def _url_grafu(channels_live_server): + autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + return ( + f"{channels_live_server.url}" + f"{reverse('bpp:browse_autor_powiazania', args=[autor.pk])}" + ) + + +def _idz_na_strone(page: Page, url: str) -> None: + """Nawiguje na `url`, obchodząc dwa źródła flakiness NIEZWIĄZANE z + testowaną logiką (odkryte empirycznie przy pisaniu tego pliku): + + 1. Baner RODO (``#CookielawBanner``, ``fixed``, wysoki z-index) renderuje + się serwerowo, dopóki request nie niesie ciasteczka + ``cookielaw_accepted`` (patrz ``cookielaw.templatetags`` w pakiecie + ``cookielaw``) — bez tego ciasteczka przechwytuje kliknięcia na + ``#bpp-przelacznik-skrotow`` i przyciskach nawigacji grafu, więc + ustawiamy je PRZED nawigacją zamiast klikać "Zgadzam się" w każdym + teście. + 2. Realne zdarzenia klawiatury (CDP ``Input.dispatchKeyEvent``) potrafią + trafić w nieaktywną kartę, gdy w kontekście przeglądarki istnieje + więcej niż jedna strona (współdzielony ``channels_live_server`` + + fixture ``page`` w wielu testach) — bez ``bring_to_front()`` + ``document.activeElement`` po naciśnięciu klawisza gubi fokus + ustawiony chwilę wcześniej przez ``locator.focus()``. + """ + page.context.add_cookies( + [{"name": "cookielaw_accepted", "value": "1", "url": url}] + ) + page.goto(url, wait_until="domcontentloaded") + page.bring_to_front() + + +# --- WCAG 2.1.4: skrót klawiszowy `/` i jego wyłącznik -------------------- + + +@pytest.mark.django_db(transaction=True) +def test_skrot_otwiera_wyszukiwarke_domyslnie( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_autora(channels_live_server)) + + page.keyboard.press("/") + + expect(page.locator("#globalSearchModal")).to_be_visible(timeout=5000) + + +@pytest.mark.django_db(transaction=True) +def test_wylaczenie_skrotu_dziala( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_autora(channels_live_server)) + + page.locator("#bpp-przelacznik-skrotow").click() + page.keyboard.press("/") + page.wait_for_timeout(500) + + expect(page.locator("#globalSearchModal")).not_to_be_visible() + + +@pytest.mark.django_db(transaction=True) +def test_ponowne_wlaczenie_przywraca_skrot( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_autora(channels_live_server)) + + przelacznik = page.locator("#bpp-przelacznik-skrotow") + przelacznik.click() + przelacznik.click() + page.keyboard.press("/") + + expect(page.locator("#globalSearchModal")).to_be_visible(timeout=5000) + + +@pytest.mark.django_db(transaction=True) +def test_przelacznik_aktualizuje_aria_pressed( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_autora(channels_live_server)) + + przelacznik = page.locator("#bpp-przelacznik-skrotow") + expect(przelacznik).to_have_attribute("aria-pressed", "true") + + przelacznik.click() + expect(przelacznik).to_have_attribute("aria-pressed", "false") + + +@pytest.mark.django_db(transaction=True) +def test_preferencja_przezywa_przeladowanie( + channels_live_server, page: Page, transactional_db +): + url = _url_autora(channels_live_server) + _idz_na_strone(page, url) + + page.locator("#bpp-przelacznik-skrotow").click() + page.reload(wait_until="domcontentloaded") + + expect(page.locator("#bpp-przelacznik-skrotow")).to_have_attribute( + "aria-pressed", "false" + ) + + +# --- WCAG 2.5.7 / 2.1.1: nawigacja po grafie powiązań ---------------------- +# +# Samo sprawdzenie widoczności przycisku nie wystarczy: przycisk zostałby +# widoczny, nawet gdyby ktoś odpiął mu handler kliknięcia. Cytoscape.js +# przechowuje żywą instancję na `container._cyreg.cy` (wewnętrzny rejestr +# biblioteki, ale stabilny w praktyce) — czytamy z niej realny stan widoku +# (zoom/pan) PRZED i PO interakcji, więc test faktycznie pada, gdy handler +# zniknie, a nie tylko gdy zniknie sam element z DOM. + + +def _cy_zoom(page): + return page.evaluate( + "document.getElementById('cytoscape-container')._cyreg.cy.zoom()" + ) + + +def _cy_pan(page): + return page.evaluate( + "document.getElementById('cytoscape-container')._cyreg.cy.pan()" + ) + + +def _czekaj_na_graf(page): + """Cytoscape inicjalizuje się synchronicznie przy DOMContentLoaded, ale + czekamy jawnie na `_cyreg`, żeby nie uzależniać testu od kolejności + ładowania skryptów.""" + page.wait_for_function( + "document.getElementById('cytoscape-container')" + " && document.getElementById('cytoscape-container')._cyreg" + " && document.getElementById('cytoscape-container')._cyreg.cy" + ) + + +@pytest.mark.django_db(transaction=True) +def test_graf_ma_przyciski_nawigacji_i_jest_fokusowalny( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_grafu(channels_live_server)) + + kontener = page.locator("#cytoscape-container") + expect(kontener).to_have_attribute("tabindex", "0") + expect(page.locator("#graf-nav-dopasuj")).to_be_visible(timeout=10000) + + +@pytest.mark.django_db(transaction=True) +def test_graf_przycisk_zoom_realnie_zmienia_widok( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_grafu(channels_live_server)) + expect(page.locator("#graf-nav-zoom-in")).to_be_visible(timeout=10000) + _czekaj_na_graf(page) + + zoom_przed = _cy_zoom(page) + page.locator("#graf-nav-zoom-in").click() + zoom_po = _cy_zoom(page) + + assert zoom_po > zoom_przed, ( + "klik #graf-nav-zoom-in nie zmienil cy.zoom() -- handler kliknięcia " + "odpiety albo usuniety?" + ) + + +@pytest.mark.django_db(transaction=True) +def test_graf_przycisk_kierunkowy_realnie_przesuwa_widok( + channels_live_server, page: Page, transactional_db +): + _idz_na_strone(page, _url_grafu(channels_live_server)) + expect(page.locator("#graf-nav-gora")).to_be_visible(timeout=10000) + _czekaj_na_graf(page) + + pan_przed = _cy_pan(page) + page.locator("#graf-nav-gora").click() + pan_po = _cy_pan(page) + + assert pan_po != pan_przed, ( + "klik #graf-nav-gora nie zmienil cy.pan() -- handler kliknięcia " + "odpiety albo usuniety?" + ) + + +@pytest.mark.django_db(transaction=True) +def test_graf_strzalka_z_klawiatury_realnie_przesuwa_widok( + channels_live_server, page: Page, transactional_db +): + # Dowód wiązania klawiatura -> nawigacja (WCAG 2.1.1): funkcja dostępna + # myszką jako "przesuń w górę" musi być dostępna też z klawiatury. + _idz_na_strone(page, _url_grafu(channels_live_server)) + _czekaj_na_graf(page) + + kontener = page.locator("#cytoscape-container") + kontener.focus() + + pan_przed = _cy_pan(page) + page.keyboard.press("ArrowUp") + pan_po = _cy_pan(page) + + assert pan_po != pan_przed, ( + "ArrowUp na sfokusowanym #cytoscape-container nie zmienil cy.pan() " + "-- obsluzKlawisz odpiety od kontenera?" + ) + + +@pytest.mark.django_db(transaction=True) +def test_graf_nie_jest_pulapka_klawiaturowa( + channels_live_server, page: Page, transactional_db +): + # Handler robi preventDefault WYŁĄCZNIE dla obsłużonych klawiszy. Gdyby + # blokował wszystko, Tab przestałby wyprowadzać focus — czyli naprawiając + # 2.1.1 stworzylibyśmy pułapkę klawiaturową i złamalibyśmy 2.1.2. + _idz_na_strone(page, _url_grafu(channels_live_server)) + + kontener = page.locator("#cytoscape-container") + kontener.focus() + expect(kontener).to_be_focused() + + page.keyboard.press("Tab") + + assert page.evaluate("document.activeElement.id") != "cytoscape-container" From a9b84b79b4bc30af1d98ef27b0c59b78485afb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Mon, 10 Aug 2026 07:03:38 +0200 Subject: [PATCH 12/20] test(wcag): usun flakiness testow grafu (pusta siec + dobicie do maxZoom) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dwie przyczyny, obie ujawnione dopiero przy powtarzanych przebiegach. 1. `_url_grafu` tworzyl autora BEZ wspolautorow, wiec BFS zwracal siec jednowezlowa, a `renderujSiec()` (graph.js:172-178) asynchronicznie chowal `#cytoscape-container`. Klawisz albo klik trafial czasem w juz ukryty kontener — 20-30% awaryjnosci na niezmienionym, poprawnym kodzie. Autor testowy dostaje teraz wspolautora (jeden wiersz AuthorConnection wystarczy: `_kandydaci_frontu` czyta relacje dwukierunkowo), wiec ta galaz nie moze wystapic. 2. `_czekaj_na_graf` czekalo na `_cyreg.cy`, ktore powstaje synchronicznie w `utworzCy()` — ZANIM fetch `siec.json` wystartuje. Bylo to czekanie na nic. Teraz czekamy na sygnal pozytywny: `cy.nodes().length > 0`, ktory zapelnia dopiero `renderujSiec()` po odpowiedzi serwera. Warunek "kontener nie jest ukryty" bylby tu bezuzyteczny — szablon nie nadaje `display`, wiec spelnialby sie przed renderem. Naprawa (1) obnazyla druga usterke: przy dwuwezlowej sieci `cy.fit()` dobija do `maxZoom` (4), wiec `zoomuj()` przycina wynik i przyblizanie jest poprawnie operacja pusta — asercja "zoom wzrosl" padala mimo sprawnego handlera. Wczesniej test przechodzil z niewlasciwego powodu: przy jednym wezle kontener byl chowany, a `fit()` na pustym grafie zostawial zoom 1. Test ustawia teraz punkt startowy jawnie (`_ustaw_zoom_z_zapasem`) zamiast liczyc na geometrie ukladu. Przy okazji: `page.wait_for_timeout(500)` w tescie wylacznika zastapiony asercja warunkowa (handler "/" jest synchroniczny), `ruff format`. Weryfikacja: 150/150 (15 x caly plik). Wartosc testow potwierdzona mutacjami — wygaszenie handlera zoom-in i warunku `obsluzKlawisz` zapala odpowiednio test przyblizania i test strzalki. Co-Authored-By: Claude Opus 5 (1M context) --- .../test_wcag_skrot_i_graf.py | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/integration_tests/test_wcag_skrot_i_graf.py b/src/integration_tests/test_wcag_skrot_i_graf.py index 6eb9c131f..75d84a329 100644 --- a/src/integration_tests/test_wcag_skrot_i_graf.py +++ b/src/integration_tests/test_wcag_skrot_i_graf.py @@ -21,18 +21,30 @@ from playwright.sync_api import Page, expect from bpp.models import Autor +from powiazania_autorow.models import AuthorConnection def _url_autora(channels_live_server): autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) - return ( - f"{channels_live_server.url}" - f"{reverse('bpp:browse_autor', args=[autor.slug])}" - ) + return f"{channels_live_server.url}{reverse('bpp:browse_autor', args=[autor.slug])}" def _url_grafu(channels_live_server): + # Autor MUSI mieć co najmniej jednego współautora: sieć BFS o <=1 węźle + # trafia w gałąź "pusta sieć" w renderujSiec() (graph.js:172-178), która + # asynchronicznie chowa #cytoscape-container (`style.display = "none"`). + # Bez współautora testy klawiatury/kliknięć poniżej są wyścigiem: klawisz + # albo klik trafiały czasem w kontener tuż przed jego ukryciem, więc + # cy.pan()/cy.zoom() się nie zmieniało (~20-30% flaky, znalezisko z + # code review — patrz raport). autor = baker.make(Autor, imiona="Jan", nazwisko="Kowalski", pokazuj=True) + wspolautor = baker.make(Autor, imiona="Anna", nazwisko="Nowak", pokazuj=True) + baker.make( + AuthorConnection, + primary_author=autor, + secondary_author=wspolautor, + shared_publications_count=3, + ) return ( f"{channels_live_server.url}" f"{reverse('bpp:browse_autor_powiazania', args=[autor.pk])}" @@ -57,9 +69,7 @@ def _idz_na_strone(page: Page, url: str) -> None: ``document.activeElement`` po naciśnięciu klawisza gubi fokus ustawiony chwilę wcześniej przez ``locator.focus()``. """ - page.context.add_cookies( - [{"name": "cookielaw_accepted", "value": "1", "url": url}] - ) + page.context.add_cookies([{"name": "cookielaw_accepted", "value": "1", "url": url}]) page.goto(url, wait_until="domcontentloaded") page.bring_to_front() @@ -79,15 +89,16 @@ def test_skrot_otwiera_wyszukiwarke_domyslnie( @pytest.mark.django_db(transaction=True) -def test_wylaczenie_skrotu_dziala( - channels_live_server, page: Page, transactional_db -): +def test_wylaczenie_skrotu_dziala(channels_live_server, page: Page, transactional_db): _idz_na_strone(page, _url_autora(channels_live_server)) page.locator("#bpp-przelacznik-skrotow").click() page.keyboard.press("/") - page.wait_for_timeout(500) + # Handler "/" jest synchroniczny (brak fetchy/await), więc jeśli miałby + # otworzyć modal mimo wyłączenia, zrobiłby to natychmiast — `expect` + # sam odpytuje aż do timeoutu, więc twardy `wait_for_timeout` jest tu + # zbędny. expect(page.locator("#globalSearchModal")).not_to_be_visible() @@ -155,14 +166,52 @@ def _cy_pan(page): ) +def _ustaw_zoom_z_zapasem(page): + """Ustawia zoom na 1 i sprawdza, że do `maxZoom` został zapas. + + Po wyrenderowaniu sieci `renderujSiec()` woła `cy.fit()`, a przy + dwuwęzłowej sieci testowej dopasowanie dobija do `maxZoom` (4, patrz + ``powiazania/cy.js``). `zoomuj()` przycina wynik do `cy.maxZoom()`, więc + przybliżanie jest wtedy — całkiem poprawnie — operacją pustą i asercja + "zoom wzrósł" pada mimo sprawnego handlera. Zamiast dobierać liczbę + współautorów tak, żeby `fit()` przypadkiem zostawił zapas (kruche: + zależy od geometrii układu i rozmiaru viewportu), ustawiamy punkt + startowy jawnie. + """ + page.evaluate("document.getElementById('cytoscape-container')._cyreg.cy.zoom(1)") + assert _cy_zoom(page) < page.evaluate( + "document.getElementById('cytoscape-container')._cyreg.cy.maxZoom()" + ), "brak zapasu do maxZoom -- test przybliżania nie mógłby niczego dowieść" + + def _czekaj_na_graf(page): - """Cytoscape inicjalizuje się synchronicznie przy DOMContentLoaded, ale - czekamy jawnie na `_cyreg`, żeby nie uzależniać testu od kolejności - ładowania skryptów.""" + """Czeka, aż `renderujSiec()` (``powiazania/graph.js``) SKOŃCZY + renderowanie sieci — nie tylko na to, że instancja Cytoscape istnieje. + + Samo `_cyreg.cy` powstaje synchronicznie przy starcie (`utworzCy()`), + ZANIM fetch `siec.json` w ogóle wystartuje, więc czekanie na nie było + czekaniem na nic: klawisz albo klik w oknie między "cy istnieje" a + "render się skończył" trafiał w pusty, jeszcze nieustawiony widok + i `cy.pan()`/`cy.zoom()` się nie zmieniało (~20-30% flaky). + + Czekamy więc na sygnał POZYTYWNY — obecność węzłów. `cy.nodes()` + zapełnia dopiero `renderujSiec()`, przechodząc `data.nodes` już po + odpowiedzi z `siec.json`, więc niezerowa liczba węzłów dowodzi, że + asynchroniczna gałąź się zakończyła. Warunek "kontener nie jest + ukryty" byłby tu bezużyteczny: ``#cytoscape-container`` nie ma w + szablonie reguły ``display``, więc `getComputedStyle` zwraca "block" + od chwili sparsowania elementu — spełniałby się PRZED renderem, + a gałąź pustej sieci (`graph.js:172-178`) ustawia `display: none` + dopiero potem. `_url_grafu` seeduje współautora, żeby w tę gałąź + w ogóle nie wejść. + """ page.wait_for_function( - "document.getElementById('cytoscape-container')" - " && document.getElementById('cytoscape-container')._cyreg" - " && document.getElementById('cytoscape-container')._cyreg.cy" + "() => {" + " const k = document.getElementById('cytoscape-container');" + " return !!(k && k._cyreg && k._cyreg.cy" + " && k._cyreg.cy.nodes().length > 0);" + "}", + timeout=15000, ) @@ -171,6 +220,7 @@ def test_graf_ma_przyciski_nawigacji_i_jest_fokusowalny( channels_live_server, page: Page, transactional_db ): _idz_na_strone(page, _url_grafu(channels_live_server)) + _czekaj_na_graf(page) kontener = page.locator("#cytoscape-container") expect(kontener).to_have_attribute("tabindex", "0") @@ -184,6 +234,7 @@ def test_graf_przycisk_zoom_realnie_zmienia_widok( _idz_na_strone(page, _url_grafu(channels_live_server)) expect(page.locator("#graf-nav-zoom-in")).to_be_visible(timeout=10000) _czekaj_na_graf(page) + _ustaw_zoom_z_zapasem(page) zoom_przed = _cy_zoom(page) page.locator("#graf-nav-zoom-in").click() @@ -243,6 +294,7 @@ def test_graf_nie_jest_pulapka_klawiaturowa( # blokował wszystko, Tab przestałby wyprowadzać focus — czyli naprawiając # 2.1.1 stworzylibyśmy pułapkę klawiaturową i złamalibyśmy 2.1.2. _idz_na_strone(page, _url_grafu(channels_live_server)) + _czekaj_na_graf(page) kontener = page.locator("#cytoscape-container") kontener.focus() From e40db389f92759884b6f3e979d838276fff39e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Mon, 10 Aug 2026 07:09:18 +0200 Subject: [PATCH 13/20] docs(wcag): domkniecie 2.1.4 i 2.5.7 w wykazie, newsfragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wykazy odroczonych niezgodnosci w obu specyfikacjach (08-05 i 08-06) twierdzily, ze 2.1.4 i 2.5.7 sa "niezgodne, swiadomie odroczone" — nadal prawdziwe w chwili pisania, nieprawdziwe od wczoraj. Oba wpisy zastapione jednym akapitem o domknieciu, z odsylaczem do specyfikacji 08-07. Poza wykazami dokument 08-05 twierdzil to samo w pieciu innych miejscach, kazde z wlasna korekta z 2026-08-06: tabela zrodel werdyktu, naglowek sekcji "Naruszenia stwierdzone", obie podsekcje kryteriow, kolejnosc prac (krok 3) i sekcja otwartych decyzji. Kazde dostalo `Korekta (2026-08-07)` zgodnie z konwencja dokumentu — bez kasowania oryginalu, zeby dalo sie odtworzyc, co i kiedy uznano za niezgodnosc. Sekcja otwartych decyzji jest tu najciekawsza: rekomendacja (zawezenie skrotu do focusa) nadal NIE zostala przyjeta, ale kryterium naprawiono inaczej — zalozenie, ze wylaczalnosc wymaga profilu uzytkownika, bylo zbyt ostrozne. Sprawdzone i pozostawione bez zmian: naglowek "Blok 5" (2.5.7 nadal jest przedmiotem audytu, teraz jako weryfikacja naprawy), wiersz 2.5.1 (osobne kryterium, gesty wielopunktowe — nietkniete), listy wykrywalnosci automatem (fakt o axe, niezalezny od stanu naprawy), opisy zakresu wymieniajace graf powiazan (nadal w probce). Wiekszosc trafien grepu z briefu to falszywe alarmy: "bibliograficzny" zawiera "graf". Co-Authored-By: Claude Opus 5 (1M context) --- ...05-wcag-22-aa-zgodnosc-frontendu-design.md | 68 ++++++++++++++----- ...6-08-06-wcag-naprawy-stwierdzone-design.md | 32 ++++----- .../wcag-skrot-i-graf.feature.rst | 9 +++ 3 files changed, 75 insertions(+), 34 deletions(-) create mode 100644 src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst diff --git a/docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md b/docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md index eb3c12ebf..bb8dd1053 100644 --- a/docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md +++ b/docs/superpowers/specs/2026-08-05-wcag-22-aa-zgodnosc-frontendu-design.md @@ -510,6 +510,12 @@ naprawione — patrz sekcja „Odroczone niezgodności". Formuła „naprawa stwierdzona → B…" dla `3.1.2` niżej jest poprawna i pozostaje bez zmian — tam naprawa faktycznie zaszła. +**Korekta (2026-08-07):** odroczenie z korekty wyżej okazało się +tymczasowe — oba kryteria naprawiono dzień później +(`2026-08-07-wcag-skrot-i-graf-design.md`). Pierwotne brzmienie wierszy +`2.1.4` i `2.5.7` w tabeli („naprawa stwierdzona → B1/B5") jest więc znów +aktualne: audyt ma te naprawy zweryfikować, nie odnotować niezgodność. + ### Zrozumiałość | Kryterium | Poz. | Źródło werdyktu | @@ -555,6 +561,12 @@ naprawione — decyzje i uzasadnienia w sekcji „Odroczone niezgodności". „Zadanie brzmi »napraw«" było zamiarem na starcie tej sekcji, nie jest opisem tego, co się faktycznie stało z każdym z czterech kryteriów. +**Korekta (2026-08-07):** ostatecznie naprawiono wszystkie cztery. 2.1.4 +i 2.5.7 domknęła kolejna iteracja +(`2026-08-07-wcag-skrot-i-graf-design.md`), więc pierwotne „Zadanie brzmi +»napraw«" opisuje jednak to, co się stało — tyle że w dwóch podejściach, +nie w jednym. + ### 2.1.4 Character Key Shortcuts (A) — skrót `/` `base.html:39-49` wiąże jednoznakowy skrót `/` na poziomie `document`. @@ -572,6 +584,14 @@ przeniesiony w zasięg focusa pola wyszukiwania. **Korekta (2026-08-06):** kryterium **odroczono**, nie naprawiono. Skrót `/` działa dokładnie tak samo jak w opisie wyżej — bez zmian w kodzie. + +**Korekta (2026-08-07):** kryterium **naprawione**. Z trzech dopuszczonych +wyjść wybrano pierwsze — wyłączalność. Preferencja siedzi w `localStorage` +(nie w profilu, więc działa też dla anonima), przełącznik „Skróty +klawiszowe" jest w stopce, a wszystkie trzy handlery `/` sprawdzają ją +przed otwarciem wyszukiwarki. Opis w akapitach wyżej („Obecny kod nie +spełnia żadnego") dotyczy stanu sprzed tej naprawy. Szczegóły: +`2026-08-07-wcag-skrot-i-graf-design.md`. Rekomendacja z sekcji „Otwarte decyzje" (zawężenie do focusa pola wyszukiwania) nie została wdrożona. Wpis w wykazie: sekcja „Odroczone niezgodności". @@ -677,6 +697,16 @@ jednowskaźnikowej. Powód: koszt nieproporcjonalny do pozostałych napraw w tej iteracji, funkcja opcjonalna i w części wdrożeń wyłączona. Wpis w wykazie: sekcja „Odroczone niezgodności". +**Korekta (2026-08-07):** kryterium **naprawione**, i to obiema drogami +wymienionymi wyżej. Graf dostał siedem przycisków nawigacji (cztery +kierunki, przybliż, oddal, dopasuj) oraz obsługę klawiaturą (strzałki, +`+`/`-`, `Home`) na fokusowalnym kontenerze — przy okazji domyka to +2.1.1 dla tego widoku. Handler robi `preventDefault` wyłącznie dla +obsłużonych klawiszy, żeby nie stworzyć pułapki klawiaturowej (2.1.2), +i przepuszcza kombinacje z Ctrl/Cmd/Alt, żeby nie przechwycić zoomu +przeglądarki (1.4.4). Szczegóły: +`2026-08-07-wcag-skrot-i-graf-design.md`. + ## Hipotezy do zbadania w audycie Poniższe **nie są stwierdzonymi naruszeniami**. To miejsca, w których @@ -908,6 +938,11 @@ nie da się ułożyć: wyżej), a `2.1.4` i `2.5.7` zostały świadomie odroczone, nie naprawione (sekcja „Odroczone niezgodności"). Szczegóły: `2026-08-06-wcag-naprawy-stwierdzone-design.md`. + **Korekta (2026-08-07):** krok (3) domknięto w drugim podejściu — `2.1.4` + i `2.5.7` naprawiono nazajutrz + (`2026-08-07-wcag-skrot-i-graf-design.md`). Cały krok „Naprawy + stwierdzone" jest więc wykonany; kroki (4)–(7) nie mają już na co czekać + po tej stronie. 4. **Baseline freeze.** Dopiero **po** (3) i po zamrożeniu próbki. Kolejność jest istotna: baseline zakładany przed naprawami zaksięgowałby dług, który zaraz znika, i wymuszałby natychmiastową aktualizację pliku. @@ -939,6 +974,14 @@ globalny, nadal niewyłączalny), wpis w wykazie niezgodności zamiast implementacji zawężenia do focusa. Powód i szczegóły: sekcja „Odroczone niezgodności". +**Korekta (2026-08-07):** rekomendacja nadal nie została przyjęta, ale +kryterium **naprawiono inaczej** — przez wyłączalność, nie przez zawężenie +do focusa. Założenie stojące za rekomendacją („dwa pierwsze wyjścia +wymagają interfejsu ustawień, a ten nie istnieje dla anonima") okazało się +zbyt ostrożne: preferencja w `localStorage` plus jeden przycisk w stopce +wystarczą, bez budowania profilu użytkownika. Skrót działa więc nadal +globalnie — dla tych, którzy go nie wyłączyli. + **Motywy uczelniane w raporcie silnika.** `vizja`, `mwsl`, `uafm` to motywy konkretnych klientów, nie warianty produktu. Jeśli test (b) wykaże w nich zły kontrast, naprawa **zmienia branding uczelni** — decyzja nie @@ -1041,22 +1084,15 @@ Zapisane tutaj, bo raport zgodności — właściwe miejsce takich wpisów — jeszcze nie istnieje. Szczegóły i uzasadnienia: `2026-08-06-wcag-naprawy-stwierdzone-design.md`. -**2.1.4 Character Key Shortcuts (A) — skrót `/`.** -Handler w `src/django_bpp/templates/base.html:39-49` wiąże `/` na -`document`, wykluczając jedynie `input`/`textarea`/`select`. Nie spełnia -żadnego z trzech warunków kryterium (wyłączalny, przemapowywalny, aktywny -tylko przy focusie). Stan: **niezgodne, świadomie odroczone**. Powód: brak -nacisku regulacyjnego i brak odbiorcy raportu; wszystkie trzy dopuszczone -wyjścia mają koszt produktowy (utrata skrótu globalnego albo zbudowanie -interfejsu preferencji dla użytkownika anonimowego). - -**2.5.7 Dragging Movements (AA) — graf powiązań.** -`src/powiazania_autorow/templates/powiazania_autorow/graf.html`, widok -publiczny bramkowany per uczelnia (`czy_pokazywac_siec_powiazan`, -`src/bpp/views/browse.py:245`). Nawigacja wyłącznie przez przeciąganie. -Stan: **niezgodne, świadomie odroczone**. Powód: koszt nieproporcjonalny do -pozostałych napraw w tej iteracji, a funkcja jest opcjonalna i w części -wdrożeń wyłączona. +Wpisy, które przestały być aktualne, zostają w sekcji z datą domknięcia +zamiast zniknąć — inaczej nie dałoby się odtworzyć, co i kiedy uznano za +niezgodność. + +**2.1.4 i 2.5.7 — domknięte 2026-08-07.** +Oba kryteria zostały naprawione w kolejnej iteracji: skrót `/` dostał +mechanizm wyłączania (localStorage + przełącznik w stopce), graf powiązań — +przyciski nawigacji i obsługę klawiaturą. Szczegóły: +`2026-08-07-wcag-skrot-i-graf-design.md`. **3.1.2 — tytuł przełożony (`tytul`).** Stan: **spełnione częściowo**. Oznaczamy tytuł oryginalny; przekład zostaje diff --git a/docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md b/docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md index dd25824d9..28a3307d8 100644 --- a/docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md +++ b/docs/superpowers/specs/2026-08-06-wcag-naprawy-stwierdzone-design.md @@ -40,6 +40,11 @@ i raportu bez odbiorcy byłoby produkcją artefaktu, którego nikt nie czyta. - 2.5.7 — nawigacja po grafie powiązań - 3.1.2 dla tytułu przełożonego (`tytul`) — patrz „Decyzja o polu `jezyk_alt`" +**Korekta (2026-08-07):** 2.1.4 i 2.5.7 nie zostały ostatecznie odroczone — +domknęła je kolejna iteracja (`2026-08-07-wcag-skrot-i-graf-design.md`). +Zakres tej iteracji, opisany wyżej, pozostaje bez zmian; nieaktualna jest +tylko prognoza, że oba kryteria zostaną z niego trwale wyłączone. + **Nie wchodzi (poza tą iteracją):** - bramka CI z axe-core, baseline, strona wzorników @@ -499,24 +504,15 @@ miejsce. Zapisujemy je w specyfikacji z 2026-08-05 (nowa sekcja „Odroczone niezgodności"), bo to dokument, który przyszły audyt przeczyta jako pierwszy; issue na GitHubie by zaginęło. -Każdy wpis zawiera kryterium, stan, uzasadnienie i datę decyzji. - -**2.1.4 Character Key Shortcuts (A) — skrót `/`.** -Handler w `src/django_bpp/templates/base.html:39-49` wiąże `/` na -`document`, wykluczając jedynie `input`/`textarea`/`select`. Nie spełnia -żadnego z trzech warunków kryterium (wyłączalny, przemapowywalny, aktywny -tylko przy focusie). Stan: **niezgodne, świadomie odroczone**. Powód: brak -nacisku regulacyjnego i brak odbiorcy raportu; wszystkie trzy dopuszczone -wyjścia mają koszt produktowy (utrata skrótu globalnego albo zbudowanie -interfejsu preferencji dla użytkownika anonimowego). - -**2.5.7 Dragging Movements (AA) — graf powiązań.** -`src/powiazania_autorow/templates/powiazania_autorow/graf.html`, widok -publiczny bramkowany per uczelnia (`czy_pokazywac_siec_powiazan`, -`src/bpp/views/browse.py:245`). Nawigacja wyłącznie przez przeciąganie. -Stan: **niezgodne, świadomie odroczone**. Powód: koszt nieproporcjonalny do -pozostałych napraw w tej iteracji, a funkcja jest opcjonalna i w części -wdrożeń wyłączona. +Każdy wpis zawiera kryterium, stan, uzasadnienie i datę decyzji. Wpisy, +które przestały być aktualne, zostają z datą domknięcia zamiast zniknąć — +inaczej nie dałoby się odtworzyć, co i kiedy uznano za niezgodność. + +**2.1.4 i 2.5.7 — domknięte 2026-08-07.** +Oba kryteria zostały naprawione w kolejnej iteracji: skrót `/` dostał +mechanizm wyłączania (localStorage + przełącznik w stopce), graf powiązań — +przyciski nawigacji i obsługę klawiaturą. Szczegóły: +`2026-08-07-wcag-skrot-i-graf-design.md`. **3.1.2 — tytuł przełożony (`tytul`).** Stan: **spełnione częściowo**. Oznaczamy tytuł oryginalny; przekład zostaje diff --git a/src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst b/src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst new file mode 100644 index 000000000..d94a0b308 --- /dev/null +++ b/src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst @@ -0,0 +1,9 @@ +Skrót klawiszowy ``/`` otwierający wyszukiwarkę można teraz wyłączyć — +przełącznik „Skróty klawiszowe" znajduje się w stopce strony. Ma to +znaczenie dla osób korzystających ze sterowania głosem, którym pojedyncze +znaki mimowolnie uruchamiały wyszukiwarkę (kryterium WCAG 2.2 AA 2.1.4). +Przy okazji naprawiono zdublowany obsługiwacz tego skrótu. + +Graf powiązań autorów da się teraz przesuwać i przybliżać przyciskami oraz +klawiaturą (strzałki, plus, minus, Home), a nie wyłącznie przeciąganiem +myszą — kryteria 2.5.7 i 2.1.1. From 47b31a39ceddaad5379ccec8bd5b3634fd4c96ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Mon, 10 Aug 2026 07:25:02 +0200 Subject: [PATCH 14/20] fix(test): pakiet `tests/` przeslanial modul `powiazania_autorow.tests` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 4 zalozyl `src/powiazania_autorow/tests/__init__.py` obok istniejacego `src/powiazania_autorow/tests.py` (17 KB, 18 testow). Python rozwiazuje `powiazania_autorow.tests` na PAKIET, wiec modul zostal przesloniety, a pytest odmawial kolekcji `tests.py` ("import file mismatch"). Skutek gorszy niz czerwona suita: te 18 testow po cichu przestalo sie wykonywac, a caly przebieg konczyl sie bledem. Recenzja Taska 4 tego nie zlapala, bo uruchamiala tylko 4 nowe testy — kolizja nazw ujawnia sie dopiero przy kolekcji calego drzewa. Konwencja tej aplikacji to moduly testowe w korzeniu pakietu (`test_views.py`, `test_pbn_root_per_uczelnia.py`, `tests.py`) — to katalog `tests/` byl odstepstwem. Plik wraca wiec o poziom wyzej, pakiet znika. Przy okazji `SZABLON` przestaje liczyc `parents[n]` od `__file__`, a kotwiczy sie na katalogu pakietu: poprzedni wariant psul sie przy kazdym przeniesieniu pliku (co wlasnie zaszlo). Weryfikacja: `pytest src/powiazania_autorow/` 63 passed (bylo 56 + 7 niezbieranych). Co-Authored-By: Claude Opus 5 (1M context) --- .../{tests => }/test_graf_dostepnosc.py | 9 ++++++++- src/powiazania_autorow/tests/__init__.py | 0 2 files changed, 8 insertions(+), 1 deletion(-) rename src/powiazania_autorow/{tests => }/test_graf_dostepnosc.py (86%) delete mode 100644 src/powiazania_autorow/tests/__init__.py diff --git a/src/powiazania_autorow/tests/test_graf_dostepnosc.py b/src/powiazania_autorow/test_graf_dostepnosc.py similarity index 86% rename from src/powiazania_autorow/tests/test_graf_dostepnosc.py rename to src/powiazania_autorow/test_graf_dostepnosc.py index 80041d2fe..6f1b95f87 100644 --- a/src/powiazania_autorow/tests/test_graf_dostepnosc.py +++ b/src/powiazania_autorow/test_graf_dostepnosc.py @@ -8,8 +8,15 @@ from pathlib import Path +import powiazania_autorow + +# Kotwiczymy na katalogu PAKIETU, nie na `__file__` testu: ścieżka liczona +# przez `parents[n]` łamie się przy każdym przeniesieniu pliku testowego, a +# ten plik już raz zmienił miejsce (mieszkał w `tests/`, która to nazwa +# przesłaniała istniejący moduł `powiazania_autorow.tests` i wywalała +# kolekcję pytesta). SZABLON = ( - Path(__file__).resolve().parents[1] + Path(powiazania_autorow.__file__).resolve().parent / "templates" / "powiazania_autorow" / "graf.html" diff --git a/src/powiazania_autorow/tests/__init__.py b/src/powiazania_autorow/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 26cd2efb94b4f7da6fa0ed46efce10d09b394921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 12 Aug 2026 22:21:22 +0200 Subject: [PATCH 15/20] test+refactor(wcag): guardy strony uczelni pod testem, trzy drobiazgi z triazu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cztery pozycje z listy odlozonych drobiazgow w ledgerze. 1. Guardy w `browse/uczelnia.html` (oba miejsca) nie mialy zadnego testu — strona uczelni ma WLASNY, inline'owy skrypt: baner reklamujacy skrot `/` oraz handler zamykajacy ten baner. Zaden test Pythona jej nie renderowal, Playwright jej nie odwiedza, wiec usuniecie ktoregokolwiek guardu przechodzilo CI. To byla jedyna pozycja z triazu, przy ktorej regresja przeszlaby niezauwazona. Trzy nowe testy, kazdy zweryfikowany mutacja: usuniecie guardu banera, guardu handlera i guardu `typeof` zapala dokladnie po jednym, wlasciwym tescie. 2. Martwy kod w `catch` w `bppSkrotyWlaczone`: warunek na wejsciu zwraca juz `stanAwaryjny`, gdy ten jest ustawiony, wiec w `catch` jest on ZAWSZE `null`, a ternary zawsze dawal `true`. Zostaje `return true` z komentarzem, czemu tak. 3. `.footer__link-button` przeniesiony do bloku `.footer` jako `&__link-button` — konwencja BEM reszty pliku. Skompilowany CSS jest bajt-w-bajt taki sam (sprawdzone w app-blue.css), to zmiana czysto redakcyjna. 4. Test skalowania kroku sprawdzal tylko os X. Dopisany blizniaczy dla osi Y: podmiana `cy.height()` na `cy.width()` w `przesun` NIE wywalala starego testu (tam wysokosc jest stala), a wywala nowy — zweryfikowane mutacja. Weryfikacja: pytest 89 (test_wcag + powiazania_autorow), vitest 152/152, grunt build. Co-Authored-By: Claude Opus 5 (1M context) --- src/bpp/static/bpp/js/skroty-klawiszowe.js | 8 +-- src/bpp/static/scss/base_footer.scss | 26 ++++----- .../tests/test_wcag/test_skrot_szukania.py | 53 ++++++++++++++++++- tests/js/nawigacja-grafu.test.js | 13 +++++ 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/bpp/static/bpp/js/skroty-klawiszowe.js b/src/bpp/static/bpp/js/skroty-klawiszowe.js index 214755471..e43bce091 100644 --- a/src/bpp/static/bpp/js/skroty-klawiszowe.js +++ b/src/bpp/static/bpp/js/skroty-klawiszowe.js @@ -28,9 +28,11 @@ try { return window.localStorage.getItem(KLUCZ) !== "0"; } catch (e) { - // Tryb prywatny, wylaczone ciasteczka, wyczerpany limit — - // czytamy fallback z pamięci sesji. - return stanAwaryjny !== null ? stanAwaryjny : true; + // Tryb prywatny, wylaczone ciasteczka, wyczerpany limit. Fallbacku + // z pamieci NIE czytamy tu ponownie: warunek wyzej juz zwrocil + // `stanAwaryjny`, jesli byl ustawiony, wiec w tym miejscu jest on + // zawsze `null`. Zostaje domyslka — skroty wlaczone. + return true; } } diff --git a/src/bpp/static/scss/base_footer.scss b/src/bpp/static/scss/base_footer.scss index aec49004e..e607b39f0 100644 --- a/src/bpp/static/scss/base_footer.scss +++ b/src/bpp/static/scss/base_footer.scss @@ -11,6 +11,19 @@ display: none; } + // Przelacznik skrotow klawiszowych (WCAG 2.1.4) ma wygladac jak sasiednie + // linki stopki — jest
+ {# Klucz do odczytu grafu stoi TUTAJ, nie w nakladce #graf-legenda: #} + {# legenda jest chowana ponizej 640 px (graf_powiazan.scss), a bez tego #} + {# zdania uzytkownik telefonu widzi kola roznej wielkosci i linie roznej #} + {# grubosci, nie wiedzac, co znacza. Chowanie TRESCI na malym ekranie to #} + {# strata informacji, nie dekoracji — czyli dokladnie to, przed czym #} + {# stoi 1.3.1. W legendzie zostaja wylacznie podpowiedzi o najechaniu #} + {# kursorem, ktorych na ekranie dotykowym i tak nie ma. #} Na każdym węźle pokazujemy najpierw współautorów z największą liczbą wspólnych publikacji. Klik w węzeł dowija jego dalszych współautorów. + Wielkość koła odpowiada wybranej metryce (liczba prac / IF / PK), + a grubość linii — liczbie wspólnych publikacji.
@@ -174,9 +183,7 @@

Sieć powiązań — {{ autor }}

color: #444; pointer-events: none; z-index: 1001;"> Najedź na węzeł, aby zobaczyć autora; najedź na linię, aby zobaczyć liczbę wspólnych publikacji. Kliknij węzeł, aby rozwinąć jego - współautorów i otworzyć menu akcji. Wielkość koła odpowiada wybranej - metryce (liczba prac / IF / PK), a grubość linii — liczbie wspólnych - publikacji. + współautorów i otworzyć menu akcji.