-
-
Notifications
You must be signed in to change notification settings - Fork 104
test(ui): die drei Breiten-Zusicherungen der Küchen-Listen absichern #643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
ce65665
9d5a7ad
e00961a
4d266c9
64426f7
534d3d3
f10d307
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1904,6 +1904,132 @@ test('die Küchen-Listen teilen eine Zeilen-Grammatik', () => { | |
| '.items-list darf kein horizontales Polster setzen: #list-content trägt schon --page-inline-pad'); | ||
| assert.doesNotMatch(shared.match(/\.kitchen-list\s*\{([^}]*)\}/)?.[1] ?? '', /padding-inline:/, | ||
| '.kitchen-list darf kein padding-inline setzen: wo der Spalten-Träger sitzt, ist pro Tab verschieden'); | ||
|
|
||
| // Die Kappung aufs Lesemaß sitzt an den KINDERN des Scrollers, nicht am | ||
| // Scroller selbst (PR #614). Die Begründung dafür stand bisher nur als | ||
| // Kommentar im CSS. | ||
| // | ||
| // Gescannt wird JEDE Regel JEDER Stylesheet-Datei, nicht der erste Textblock | ||
| // je Selektor. Zwei Wege führen sonst am Guard vorbei: ein zweiter Block | ||
| // hinter einem Breakpoint, und das Modul-CSS, das später lädt und auf | ||
| // demselben Element sitzt (`class="kitchen-list items-list"`). | ||
| const styleDir = new URL('../public/styles/', import.meta.url); | ||
| const allRules = readdirSync(styleDir).filter((f) => f.endsWith('.css')) | ||
| .flatMap((file) => cssRules(read(`../public/styles/${file}`)).map((rule) => ({ file, ...rule }))); | ||
|
|
||
| // Der WIRKSAME Wert einer Eigenschaft, oder null. Drei Fallen stecken darin: | ||
| // | ||
| // - Eine Deklaration ist kein Textvorkommen: `--eigene-max-width: 40rem` | ||
| // setzt keine Breite, und `--x: var(--content-max-width-narrow)` erfüllt | ||
| // keine Zusage. | ||
| // - Die LETZTE Deklaration gewinnt, wie im Browser. Sonst gilt | ||
| // `max-width: var(--content-max-width-narrow); max-width: none` als | ||
| // erfüllt, obwohl das Element bildschirmbreit läuft. | ||
| // - Kurzschreibweisen setzen dieselbe Eigenschaft mit: `place-self: | ||
| // stretch` setzt `align-self` zurück. Deshalb nimmt die Funktion eine | ||
| // Liste und gibt bei `place-*` den ersten Teilwert (die Block-Achse). | ||
| const declaredValue = (body, props) => { | ||
| const alternatives = [].concat(props).map((p) => escapeForRegExp(p)).join('|'); | ||
| const hits = [...body.matchAll(new RegExp(`(?:^|;)\\s*(${alternatives})\\s*:\\s*([^;]+)`, 'gm'))]; | ||
| if (!hits.length) return null; | ||
|
ulsklyc marked this conversation as resolved.
|
||
| const [, prop, raw] = hits[hits.length - 1]; | ||
| const value = raw.trim(); | ||
| return prop.startsWith('place-') ? value.split(/\s+/)[0] : value; | ||
|
ulsklyc marked this conversation as resolved.
Outdated
|
||
| }; | ||
| const NARROW = 'var(--content-max-width-narrow)'; | ||
|
ulsklyc marked this conversation as resolved.
|
||
| const ALIGN_SELF = ['align-self', 'place-self']; | ||
|
|
||
| // Zielt der Selektor auf das Element selbst, nicht auf einen Nachfahren? | ||
| // Geprüft wird der LETZTE Compound, damit auch `.kitchen-list#items-list`, | ||
| // `.kitchen-list:hover` und `:is(.kitchen-list)` als Treffer gelten - | ||
| // `.kitchen-list .row` dagegen nicht. `:not(…)` fällt vorher weg, sonst | ||
| // meldete ausgerechnet der Ausschluss einen Treffer. | ||
| const targets = (selector, cls) => { | ||
| const compound = selector.trim().split(/[\s>+~]+/).pop()?.replace(/:not\([^)]*\)/g, '') ?? ''; | ||
| return new RegExp(`\\.${escapeForRegExp(cls)}(?![\\w-])`).test(compound); | ||
|
ulsklyc marked this conversation as resolved.
Outdated
|
||
| }; | ||
| const rulesFor = (cls) => allRules.filter(({ selectors }) => selectors.some((s) => targets(s, cls))); | ||
|
|
||
| // 1. Der Scroller selbst darf nicht gekappt werden. Er ist das Element mit | ||
| // `overflow-y: auto`; kappt man es aufs Lesemaß, endet damit auch sein | ||
| // eigener Trefferbereich fürs Mausrad an der Lesespalten-Kante, und auf | ||
| // einem breiten Fenster greift das Rad rechts davon ins Leere. | ||
| // | ||
| // Welche Klassen den Scroller mitbenennen, sagt das Markup, nicht diese | ||
| // Liste: wer auf demselben Element sitzt, kann seine Breite kappen. | ||
| // JEDE geprüfte Seite muss ihre eigene Kombination liefern. Eine globale | ||
| // Mindestzahl genügt nicht: fiele nur eine Seite aus der Erkennung, würden | ||
| // die beiden anderen sie weiter erfüllen, und deren Modul-Klasse wäre | ||
| // ungeprüft. | ||
| const scrollerClasses = new Set(['kitchen-list']); | ||
| for (const page of ['shopping', 'pantry', 'recipes']) { | ||
| const combos = [...read(`../public/pages/${page}.js`) | ||
| .matchAll(/class(?:Name)?\s*=\s*(['"`])([^'"`]*\bkitchen-list\b[^'"`]*)\1/g)]; | ||
| assert.ok(combos.length > 0, | ||
| `${page}.js hängt seine Klasse nicht mehr literal an .kitchen-list - dieser Scan findet sie dann nicht und prüft den Scroller des Tabs ungewollt gar nicht`); | ||
| combos.forEach(([, , combo]) => combo.trim().split(/\s+/).forEach((cls) => scrollerClasses.add(cls))); | ||
| } | ||
| assert.ok(rulesFor('kitchen-list').length > 0, | ||
| '.kitchen-list ist nirgends definiert: ein leerer Treffer darf hier nicht still grün bleiben'); | ||
| for (const cls of scrollerClasses) { | ||
| for (const { file, selectors, body } of rulesFor(cls)) { | ||
| assert.equal(declaredValue(body, 'max-width'), null, | ||
| `${file} ${selectors.join(', ')}: der Scroller darf nicht gekappt werden, sonst endet sein Mausrad-Trefferbereich an der Lesespalten-Kante`); | ||
|
ulsklyc marked this conversation as resolved.
Outdated
ulsklyc marked this conversation as resolved.
Comment on lines
+2041
to
+2042
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assertion rejects every size declaration regardless of its value, so a harmless module rule such as Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| // 2. Tragen muss die Kappung stattdessen jedes Kind, das ALLEIN Kind des | ||
| // Scrollers sein kann: .kitchen-group bei gruppierten Tabs (Einkauf, | ||
| // Vorrat), .kitchen-rows ungruppiert (Rezepte). Fehlt sie an einem der | ||
| // beiden, läuft der betroffene Tab bildschirmbreit - und ein zweiter | ||
| // Block darf sie auch nicht auf einen abweichenden Wert ziehen. | ||
| for (const cls of ['kitchen-group', 'kitchen-rows']) { | ||
| const rules = rulesFor(cls); | ||
|
ulsklyc marked this conversation as resolved.
Outdated
|
||
| assert.ok(rules.some(({ body }) => declaredValue(body, 'max-width') === NARROW), | ||
| `.${cls} muss das Lesemaß selbst tragen: es kann alleiniges Kind von .kitchen-list sein, und .kitchen-list kappt nicht mehr`); | ||
| for (const { file, body } of rules) { | ||
| const width = declaredValue(body, 'max-width'); | ||
| if (width === null) continue; | ||
| assert.equal(width, NARROW, | ||
| `${file}: .${cls} bekommt hier eine zweite, abweichende Breite - das Lesemaß ist EIN Wert`); | ||
| } | ||
| } | ||
|
|
||
| // 3. Und wer aufs Lesemaß kappt UND clippt, muss auf seine Inhaltshöhe | ||
| // wachsen dürfen. | ||
| // | ||
| // Absichtlich eine Regel und keine Allowlist: `overflow: hidden` (hier für | ||
| // die Eckenradien) macht aus dem gekappten Kind einen Clipper. Ohne | ||
| // `align-self: start` streckt das voreingestellte `align-items: stretch` | ||
| // es auf die volle Spurhöhe, und es schneidet alles darüber still ab, | ||
| // bevor .kitchen-list den Überlauf je sieht. Gemessen an einer Rezeptliste | ||
| // mit 50 gespiegelten Einträgen: scrollHeight 3249px gegen clientHeight | ||
| // 657px, kein Scrollbalken, kein Weg an die übrigen Zeilen. Harmlos ist | ||
| // das nur, solange mehrere kurze Gruppen dieselbe Spur teilen. | ||
| // | ||
| // Der Scan bleibt auf kitchen-row.css, wo die geteilten Bausteine | ||
| // definiert werden. Andere Module kappen mit demselben Token Elemente, die | ||
| // nie Grid-Item dieses Scrollers werden (shopping.css die Eingabezeile, | ||
| // layout.css den Leerzustand) - für die wäre `align-self: start` falsch. | ||
| // Innerhalb dieser Datei gilt dieselbe Einschränkung für .kitchen-bulkbar: | ||
| // sie steht ÜBER dem Scroller (siehe dort) und trägt das Lesemaß, clippt | ||
| // aber nicht. Käme dort ein `overflow: hidden` dazu, meldet dieser Guard | ||
| // einen Fall, den ein Mensch entscheiden muss. | ||
| for (const { selectors, body } of cssRules(shared)) { | ||
| if (declaredValue(body, 'max-width') !== NARROW) continue; | ||
| if (declaredValue(body, 'overflow') !== 'hidden') continue; | ||
|
ulsklyc marked this conversation as resolved.
Outdated
|
||
| assert.equal(declaredValue(body, ALIGN_SELF), 'start', | ||
| `${selectors.join(', ')} kappt aufs Lesemaß und clippt zugleich, ist also ein gekapptes Kind des Scroller-Grids: ohne align-self: start schneidet es den Überlauf ab, bevor .kitchen-list ihn sieht`); | ||
| } | ||
|
|
||
| // Und kein später geladenes Modul-Stylesheet biegt den Wert wieder um - | ||
| // auch nicht über die Kurzschreibweise place-self. | ||
| for (const { file, body } of rulesFor('kitchen-rows')) { | ||
| const align = declaredValue(body, ALIGN_SELF); | ||
| if (align === null) continue; | ||
| assert.equal(align, 'start', | ||
| `${file}: .kitchen-rows bekommt hier ein anderes align-self - genau der Rückfall, den die Regel darüber verhindert`); | ||
| } | ||
| }); | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.