From 59dfb2fb67af502b12b4a9ebd93c89fdd1ababcc Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:38:22 +0000 Subject: [PATCH 1/6] Remove the Feed fish button from the pond controls --- components/concepts/signal-desk/pond-input.ts | 15 ----------- .../signal-desk/signal-desk.module.css | 25 ------------------- .../concepts/signal-desk/signal-desk.tsx | 9 +------ 3 files changed, 1 insertion(+), 48 deletions(-) diff --git a/components/concepts/signal-desk/pond-input.ts b/components/concepts/signal-desk/pond-input.ts index 8ba6282..d4ffd2f 100644 --- a/components/concepts/signal-desk/pond-input.ts +++ b/components/concepts/signal-desk/pond-input.ts @@ -101,19 +101,6 @@ export function installPondInput(options: PondInputOptions) { if (activeTouch?.id === event.pointerId) activeTouch = null; }; - const feedFromControl = (event: Event) => { - for (const y of [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]) { - for (const x of [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]) { - const screen = { x: innerWidth * x, y: innerHeight * y }; - const obscured = [...document.querySelectorAll("[data-pond-obscures]")].some(element => { - const box = element.getBoundingClientRect(); - return screen.x >= box.left && screen.x <= box.right && screen.y >= box.top && screen.y <= box.bottom; - }); - if (!obscured && ownsPoint(screen) && dropFood(screen)) { event.preventDefault(); return; } - } - } - }; - const resize = () => { activeTouch = null; }; @@ -122,7 +109,6 @@ export function installPondInput(options: PondInputOptions) { interaction.addEventListener("pointerdown", press, { passive: true }); interaction.addEventListener("pointerup", release, { passive: false }); interaction.addEventListener("pointercancel", cancel, { passive: true }); - interaction.addEventListener("pond-feed", feedFromControl); window.addEventListener("resize", resize, { passive: true }); return () => { @@ -130,7 +116,6 @@ export function installPondInput(options: PondInputOptions) { interaction.removeEventListener("pointerdown", press); interaction.removeEventListener("pointerup", release); interaction.removeEventListener("pointercancel", cancel); - interaction.removeEventListener("pond-feed", feedFromControl); window.removeEventListener("resize", resize); }; } diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css index 5f6b3f5..fc8ea90 100644 --- a/components/concepts/signal-desk/signal-desk.module.css +++ b/components/concepts/signal-desk/signal-desk.module.css @@ -350,28 +350,6 @@ border-radius:50%; } -.feedButton { - display:inline-flex; - align-items:center; - gap:6px; - min-width:44px; - min-height:44px; - margin:0 0 0 2px; - padding:0 12px; - border:1px solid var(--line-strong); - border-radius:2px; - background:var(--paper); - color:var(--ink); - font-size:14px; - font-weight:650; - cursor:pointer; -} - -.feedButton:disabled { - opacity:.6; - cursor:wait; -} - .pondNote { position:absolute; right:20px; @@ -751,9 +729,6 @@ .pondChoice span { padding:0 8px; } - .feedButton { - padding:0 9px; - } } @media(forced-colors:active) { diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx index 0addf20..d2637c7 100644 --- a/components/concepts/signal-desk/signal-desk.tsx +++ b/components/concepts/signal-desk/signal-desk.tsx @@ -215,7 +215,6 @@ export function SignalDesk() { return () => { window.removeEventListener("scroll", sync); window.removeEventListener("hashchange", sync); }; }, []); const [pondStatus, setPondStatus] = useState<{ mode: string; renderer: string }>({ mode: "ascii", renderer: "loading" }); - const [feedStatus, setFeedStatus] = useState(""); const entrance = ready && !reduced; useEffect(() => { @@ -231,7 +230,7 @@ export function SignalDesk() { return (
-

The decorative pond responds to pointer movement. Click or tap open water to feed the fish, or use the Feed fish button in the pond controls.

+

The decorative pond responds to pointer movement. Click or tap open water to feed the fish.

@@ -358,11 +357,6 @@ export function SignalDesk() { ))}
-
{pondStatus.mode !== pondView || pondStatus.renderer === "loading" ? `Loading ${pondView === "ascii" ? "ASCII" : "Pixel"} view…` @@ -370,7 +364,6 @@ export function SignalDesk() { ? "Pond unavailable. Try the other view." : `Current view: ${pondView === "ascii" ? "ASCII" : "Pixel"}`}
- {feedStatus} From 5eb53e3d214025f4d71eac80866fa6dc78dbcb41 Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:25:38 +0000 Subject: [PATCH 2/6] Restore keyboard and screen reader feeding without the visible button --- components/concepts/signal-desk/pond-input.ts | 29 +++++++++++++++- .../signal-desk/signal-desk.module.css | 33 +++++++++++++++++++ .../concepts/signal-desk/signal-desk.tsx | 12 ++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/components/concepts/signal-desk/pond-input.ts b/components/concepts/signal-desk/pond-input.ts index d4ffd2f..3e565b6 100644 --- a/components/concepts/signal-desk/pond-input.ts +++ b/components/concepts/signal-desk/pond-input.ts @@ -16,7 +16,8 @@ function isCompletedTap(touch: { moved: boolean; started: number }, now: number, } // A single click or tap on open water splashes and drops food. The browser's -// context menu is never intercepted. +// context menu is never intercepted. Keyboard users feed through the +// "pond-feed" event (see feedFromControl). export function installPondInput(options: PondInputOptions) { const { interaction } = options; @@ -101,6 +102,30 @@ export function installPondInput(options: PondInputOptions) { if (activeTouch?.id === event.pointerId) activeTouch = null; }; + // Keyboard and screen reader feeding. The pond surface is aria-hidden, so a + // visually hidden "Feed fish" control dispatches a cancelable "pond-feed" + // event here. Search a centre-out grid for open water that no page content + // ([data-pond-obscures]) covers and drop food at the first hit. Calling + // preventDefault tells the dispatcher that food was dropped. + const feedCandidates = [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]; + const isObscured = (screen: PondPoint) => + [...document.querySelectorAll("[data-pond-obscures]")].some(element => { + const box = element.getBoundingClientRect(); + return screen.x >= box.left && screen.x <= box.right && screen.y >= box.top && screen.y <= box.bottom; + }); + + const feedFromControl = (event: Event) => { + for (const y of feedCandidates) { + for (const x of feedCandidates) { + const screen = { x: innerWidth * x, y: innerHeight * y }; + if (!isObscured(screen) && ownsPoint(screen) && dropFood(screen)) { + event.preventDefault(); + return; + } + } + } + }; + const resize = () => { activeTouch = null; }; @@ -109,6 +134,7 @@ export function installPondInput(options: PondInputOptions) { interaction.addEventListener("pointerdown", press, { passive: true }); interaction.addEventListener("pointerup", release, { passive: false }); interaction.addEventListener("pointercancel", cancel, { passive: true }); + interaction.addEventListener("pond-feed", feedFromControl); window.addEventListener("resize", resize, { passive: true }); return () => { @@ -116,6 +142,7 @@ export function installPondInput(options: PondInputOptions) { interaction.removeEventListener("pointerdown", press); interaction.removeEventListener("pointerup", release); interaction.removeEventListener("pointercancel", cancel); + interaction.removeEventListener("pond-feed", feedFromControl); window.removeEventListener("resize", resize); }; } diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css index fc8ea90..a435c2d 100644 --- a/components/concepts/signal-desk/signal-desk.module.css +++ b/components/concepts/signal-desk/signal-desk.module.css @@ -76,6 +76,39 @@ border:0; } +/* The keyboard and screen reader feed control: visually hidden like + .srOnly until it receives keyboard focus, then it appears as a pond + control chip (same pattern as the skip link). */ +.feedControl { + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + white-space:nowrap; + border:0; +} + +.feedControl:focus-visible { + position:static; + width:auto; + height:auto; + min-height:44px; + padding:0 11px; + margin:0; + overflow:visible; + clip:auto; + white-space:normal; + border:1px solid var(--control-edge); + border-radius:2px; + background:var(--paper); + font-family:var(--font-space-mono),monospace; + font-size:13px; + cursor:pointer; +} + .header { position:fixed; z-index:30; diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx index d2637c7..7df3cc7 100644 --- a/components/concepts/signal-desk/signal-desk.tsx +++ b/components/concepts/signal-desk/signal-desk.tsx @@ -215,6 +215,7 @@ export function SignalDesk() { return () => { window.removeEventListener("scroll", sync); window.removeEventListener("hashchange", sync); }; }, []); const [pondStatus, setPondStatus] = useState<{ mode: string; renderer: string }>({ mode: "ascii", renderer: "loading" }); + const [feedStatus, setFeedStatus] = useState(""); const entrance = ready && !reduced; useEffect(() => { @@ -230,7 +231,7 @@ export function SignalDesk() { return (
-

The decorative pond responds to pointer movement. Click or tap open water to feed the fish.

+

The decorative pond responds to pointer movement. Click or tap open water to feed the fish, or use the Feed fish button next to the pond view options to drop food with the keyboard.

@@ -357,6 +358,14 @@ export function SignalDesk() { ))}
+ {/* Visually hidden: the pond surface is aria-hidden, so this is the + keyboard and screen reader route to feeding. Pointer users click + or tap the water instead. */} +
{pondStatus.mode !== pondView || pondStatus.renderer === "loading" ? `Loading ${pondView === "ascii" ? "ASCII" : "Pixel"} view…` @@ -364,6 +373,7 @@ export function SignalDesk() { ? "Pond unavailable. Try the other view." : `Current view: ${pondView === "ascii" ? "ASCII" : "Pixel"}`}
+ {feedStatus} From 155a448310a8348e2f0b3c777ff9a1ca229d279e Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:14:42 +0000 Subject: [PATCH 3/6] Fix browser verification drift after homepage design review --- app/fonts/instrument-serif-latin-400.woff2 | Bin 0 -> 21032 bytes app/globals.css | 4 +- docs/verification/design-92-browser-drift.md | 71 ++++++ tests/browser/browser_test.py | 227 ++++++++----------- tests/browser/issues_test.py | 2 +- 5 files changed, 164 insertions(+), 140 deletions(-) create mode 100644 app/fonts/instrument-serif-latin-400.woff2 create mode 100644 docs/verification/design-92-browser-drift.md diff --git a/app/fonts/instrument-serif-latin-400.woff2 b/app/fonts/instrument-serif-latin-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0ad69719cac6f45ed3057825095b0cdffa7d3c99 GIT binary patch literal 21032 zcmV)xK$E|BPew8T0RR9108%Ib5dZ)H0KMb@08zvM0RR9100000000000000000000 z0000Qf&d%GNF0PB24Db$atJgDoD2~N3W4${fz4qHgE{~KHUcCAhHwNR1%)UFg&hom zCmXt`HL}fV#ktjrg&W5yvt6m6a1e#3LouP48b(9K`-SPAW?80>n6_C# zBp0q+9`G67UIPUhNhQ8;mhnu#E+5gI-bt#;B4m1rt=P^JC11MA)Ry3VxxQyiu9H_2#DdL~MkIlL7{jgm)4k#{+)(tq4 z1eRSr?Na}7fbRWxBwz_?L?Vz(#xq{Sb|MulmblP2u)p4ehWgteKNtrvEkg}r1worD zlyX9-n;6WA2qXUer<&{ko~jm$k#t1yh*%*_&gf`w~&P)rKsfMuP01ZA>z)^3t}WD7N_bpW;6 z_M6X;90Cb1sMQpjw`!@=n$>f*Dbto>@3a-3`@WZ-5ftSYuEJRmRD(FsEF%!Is(t}L zQM+Sp9@7U)6Y!l^_pSM-KrL2HYH1@C{c`7hFfuZ%48wxYEAa50`fqLSi+0gm77Ng6 zv0zy9+P}7xiz;CSM)~1;y8$w6NJmD{4@$oE6}5|6T{V&!oJ>LA*V4n(%~H|XX>BSU zUV6P#{+8{OiQeo$w8JuDHC?w6Z5v?vh`zb_eQTxd>i_xzSYZpSNEl!@F)l)8tCxCe z;ea}TYQhs{2`FS_gap4gMn1|eM`msV1Gn@Sr!rB2DP zWSyNY8RjuO|KGA^xn#RSez~%R1M8aVA-q$N)ev=&`=G`VlEPy&3b~Sy74+cW^xyVc zppdjmqUO!B05~9xigx2~$F#^cF|#Lxn1pB6@G`>yY=Std;DmTwH;W0`u}DjwPX zO$7oEKsCT?7?Lo-{O}Nfddvg}B%ohT;ir0b1O*shH-G2oHNE-ygg_7wiVeiY1>*As z67vI+PylI!0hyS9EUZ8dE}%qipfqVf9yvhy3V@1K0@bMpYSaeQp#!K(cej%XPCFCM zF9zMnh{lXZzT<8HQJ}v-Zpc%f?+*f$Q}7@R-E(IF_{@{eW&lS5kl*Y7^&8le|dNO990L3k&vHB^gX$n2`P`Sg;E5%XnePWc&!QU_cXTC(z3@0W@;2 zfR5aDjHipp^&Ox=i95jp4o)+jF>n;1M8ejCQCAtdDf6hNX-$&WXad3AktUoky~qkH zZ6HM^Em83XF-9Wz=p~z;&NMT*HsvV_Ib@zB$3cvZ<@qQ>)CrCxKH(5>9_U(=WawyyDF# zJ$BcaE74q$MuEYt+dwzt9nkf3@n&BM-xcN}9^@i=Zfl0qC;!FEwm-hrgc>Qm3#1jnQpb))Q!ep<9 z0Fw>~Ot8R*5K^cRf^b+kMh0TxfD7r!K(6M^IEu7nLPGaI9jP@nP3uq9(0J$H7{L)5 z_r1aNZ-D?ps07FlyNm*m07)hO>_tykUrny({)~%LI<-1|xO^#hjXK=)=X0hV7&CHrZgQ?2JijLqj^bm}X6uH->07m6cb4I&9qcb1 zoo4O=f3q+fxF8-nb1A9GK&3B-oDRwad?wa9Oh;VCv2*JkHI}QPZSau3vQb4;Sbj&j z+cp%eJcHvED3d0a0aWt*eWXr~*kxkXEp&MsdKr#fx2 zwl);U5;NDl<(68e!phdGY_?UkJr1aI*byzxIIDfL8|a+~Oh6C{1cCsCA)(PFUkB#(ao7lvBMdstpfifYBpS?-3`R*r^x1JlbZkKi z-v|+jWyBoFPq}yr&lHH1D2YIX4`>iCOa;b5wg|Gr1TGdxxSDXmuOp~KzF4Y2*^mj| zRf(V~6;J|R;;(Ojf`#AWDa!L5*kFOBBBWqJ0)I&0j{qgWj5KGL($l}gfMG)d*#ESG zCU>%?vnoyUNOtEMSAp>jW0Em8_c_Kc#u~bs&RU6;ES2rJC7y;$ag6pA?RJ_6J%$dz zE8r=s=fH`RZEzTDQ`Ngf2S6D0U+ND`g@#aH_TQ&oEQMBu!%|z$Q3t3isJ%>Rrq(V% zDW`bI$H@EMWGif=UL-qt*)n@6S|S(0LLiOJ59gxUK5K7t#!gXvS7Vd%CZ0>)p1T=4 zRF6p7wYZA0&fzHtSU@U(uDYrG|Kw@r=+$Gc#b(%sd9mMW^8#ymp+Q|_WL#_rUSpJXk31gv3e;%Sq*;qr zZQ6C{)b0HLP=zlu545l7oR~W{f1?HE+6~O)=wiyakV=EYWW5oj4{d|8Zo{T6+tM=c zXF)0`rsPqk0acu;Qw!=Enp%Q8=X?P&2m(W(F!(Ofo@AtK@!(gWh9ZqL3+?fvtlzW< zUDKV$XQ*@H`EsIj^E=5HBE7ZxXyIaFW|{kxXsZ#_%-J=OEoEC~EA6p&4eQgI=TD!H zTR^0tR)i2TD~HcA5)P)B$2A|-Z7nSqLk9L23de>Q540? zv3ijys;a7&YkqB|lc@Qz9(_M ziv{EX0|*R(!r%xb3XQ?y@C4$=vNY5Pln_zHw5X)4qN=8zMsqXdp0?8={WODMXk@$$ zzZv00>MLI|%pB^0^3bC;YF-t2t#eTFMfs}wM!)x@be(B}X1aB!>?y4uX|qbk8@6>C z%*-t;$DMR<-QRmn>PcDCPiV&uyLJ*HYwDYZT~}WgZV|1S00;&U7y^aCi-r zR>Wl*xwWX>i54SPoOo8YZuc7i0001HxGQx=T2J>K!!R?lWXq8&FZu0W;6p(W00000 z0A{LYS#9c))Q4%qjK(=lnzd-vR_%MMBb`U-lBt=dbSpEC`^*3V0JNY0KLTfV>-;Q; z|HL)!gk$aqXT1QJKOzJ~BxDp+&=ko6%?vAA7C1TvCKfhMxJ^8K0zy7~iTSm@B55bU zqyotV2_~nYEY(Cv)I_uGYPX@A(u;wyhiwj^5i)S5004r(5GV{@q=7y=bVnb0$Sw&S%J&mST+Ir9#9kzy8!aUFOydZFbAZ(ADah#clnOctw`h>~Sl0YL9-$r%1^=!{M0VPxTIX1Vqi-ce(_`DdFF{CsXzVNDZ3O+h!J zPK&8!bn0o_=Es_cNCjybSvh%yTSr@S@c z(565r5=*4GOcjAzLsB%8mUN^i!)6+D|GCI&b3C`H@3s|TbxuA{kt&78U~zZ?kxHX8 zm@GDjD^^reR#8<`*Q!1l7#bNjzxAo-=EU40OK%o1rI$1Y)maaI%bDe|@nVlPJ?G(B zyu|fT-P7XM&g@dg=U3Rao)We5o?9}hAT1*+C$CT&^o`c+u+uKP?XlNB`yHy&ZZ1h@@f?mr-X|f*)r|LkYu>|ZX?OIG`baiq4(dnP{io?*pL!!- zdLutN?r!AxLx^hx5y z{7!VcC!JbdxO(3|{9K*ySU9(EcHzvz=`_{vFIcSHLfxu%rqErQX zj*~q=AcFvc?vfXXq-sk zmdHZ7=4MT46yJ)Ek>~~}_-`EwUZgJ)c#uK4C5ic-*U3eSJz0eeNNXz(VEC5@#{|J? zyjE8LO=%1}$$uR7P+*kgnMIuZ2f+OFm;kl`i2v!2;|CG}65uqoE+UXgsbnDcB|#z* zD#C0#?RLOnN1brmd$2%#GV4k`sm}*UBoQIWw!}x`Cy`2mC6SVYt)x=c`~T3APNX5b z?e(FfjydH#4(DZnSTJ=-vPv2hu0AkQ|qR#vC3iVopIgAMhw|*ol(nd zw$$LCKmL#Ric2nA;|LG}MMKBI#lt5e=Et9el!}^$Rw$itCT13qcG+OJYqohHik*W? zqEuSnv$0_2C5(}-p2;j;YXPE)ksKLoNw=gu=@kOmgR2N|HIFMY zh!tCr1MMjv-kNY-`iT%+X2Y(_Th%XQ+&cj+C1W?Xl&((-tFXP`xORMC`{IjxH2L`f z0&(JsKjMN?LYn(yEx~vo3#2&iwCPE01VKC64K--N=hu|duab$*UVqf5viFwUbmzCP zi8pB}oL@(nm^602uIRidaV#)?udtIG?H18;q9md3Wkm{~L*8!g6-5gy>p*+*J z=pt+5553bzGEnpZ+57IbH6LK6o0H4J6@p80Ateji9%-YX6}d(+wWx~fH3+c<$*s)Q zBh(w$EIw|wLdtjsZnUg&r>-6moh@&TSbc4|lLwhq9KKY%sG=8x6QFyzGLOxVs#AKq z;JNT>4fxbE7EM`PNueTb6#5vD?qycYmTiPfwrHb(9kw_}jMq>h7J-*re>= zVgQ*|y8F%*nM4#vpX zrH4a$!Im;mSvHECFcbNuN02lshI*PqbnXne0x*&>{hZB?clA2T@4^F-Zn-NdXB-1xZN*DM_E7hiIWF zmAty=1vT;EGiq-xpsW|P{LWllZ`<;BhOJ_PYhP^}QO@sBleS80)~s45Hi%7Pi`XX8 zM25%`IYPR%1T59*g7SLe0iXdsvsf8C(8}*&0VrPVxNZYD9cad4(Jwt7E(uU70TTXur^@v9JBNKW% zt1F!d?3=GKz6Vl}q17kZ14b^Ntze_c&T%R!(4~4 zr6qEUsWT!+g*7W}#_6Ju-&x4dt2dH^Tf3z>KK2Be5V*m% zvh@_~dgg2k1^a@dF-T>Zb;i2`v+m#&$hjz_XUVfR{!kDF7A9oUN0GIOhk_`v$fr0Z z=`v+tlN|=49ANTeNTvd8io-xu0@5;N8e5^Es_!;6z|@)ahXz`k=1>r|EVRuXfewK# zfgXWAfdPRbfe~@r&ls7G8uv-pN!B;Tx0&dd6cg+GcfgK?!S|(d!dP8YeOp&H)v;w_ z>;P+zcOb^`qV(!ioQq3wZRFO-y^+VzXsqgfH(?p6VT)zK+A7H!Dc{O2x#>M5D@aA>c(jp0oEYF z&VEjfA<b3`vqi6MDzJtauqYH$(d0(l4-uo0c8ux5q|#X?OIqh*KoTld385z1^>)?b zW}_!0N|Wx5(>2jrXlWiD%77S{Bs3~g?}rGbbh;S9xI7HwD5k3B$}ntJFA~XQJjl7; ztOQ0P9hbWp9FCg9i9y3s85pL8tPnLl4=WL2hS(x5SHvti11zM|nVd&MPCvZ!vX_9= zK9sf;p!6c;G%EJ*R0<4Hx=4CLn#Qt%dyKdjTOj;7zO!>-xkVG>IDY6Dp3SQiHM;{a zmrPFnI`Yv{eLly`+}R}<5TxnP;j0z{7*=z0yT=N^AeprF6JS%E*{HWJv6p4kb$Es= zu5+O=F_o-9e!bsI%O}buw$ywTv54~yiC!tqwHnaADgWco-E3IkIztYTKJl)On&)tw z67@REU%U!9hDWefX9H_B|F+bE4UE~e^1sf6LiPRpn&Qw%tias#GM*AaGmxc-rqXq+ zE{SD3+ko-n&z3OYK@*KJyvG!qNh(!t8|}5sgrxe8FDed*&{V&`DSL&N=@Yc(j;Ty| z?M&Dkt5LjOV@lU^BgipZ%oIZ~jS_&Q)7Xb*d182PZEPTiqQRx(Kb>=CFR$Y+C5$=z zcK+HQP*t>7bU=$#v-sCiyVQ=+gi4;OGI)viP`7gU2hi`yoELyEGG)eS~Z)xhT;l|dpTAY2+^6t;u*0sh>cAo9B=Wr=jJHKF9r^-ez z@aPAC!I7VVgVVUxW+r?;Wz_kq{EwBmYid*1_h`yG`esC_smtd5M|ukq2i-lS{YWLyjINb8{1%u?AHV zU@!6qOmI9MNSbq$278-C(sWFfT=baae0@O^fpK(c`E#Rh=IuJHBdJ(6aTfEgh^ zRx#c-%k=pkgwl?g;doicT}BvtXS%~Ft)u!0@w(ydqcT3cklX4k8l?o({z%&J~QNy_g z0vv{{@)@m{%n$65G?F8`%&XABbx*|S^QtPvI9b7QE>(z*p`Bs?1p3w-2rNlm{A-D! zEr__6R3C7Y1$t;1$cjf5&R;Ax(>RS9ctS9ylG0~ovOio!B4Hw)8oGy4yp!^sbV{%F zFk`I!e@8AEXPWrI*}^H?wH_->rKB0tP_%A|xrB0*HXDm-1R;a96i#A(@48wy5%9|pe*CnETEfwy6p#Fq|M8Q2|APcSU3B1LxM934-IfxkvVU8&(%s8>9*|f zQ+@_MSTNf)?wHKv!%Nu!@f$BuT>@W8ik^aNo1pyU#rr>jIee~BiL(d_)2jEr7d*%O zNJ~{Z9i1tcXBY+dvq8bc#K4eBus+_Df3$A=N7d!`IKr-Q}TeM5piY`;qxZ~1c)gzRuqi=Mx^0qiee~dnPj^)IMTF6 z-L_+FIkkc$O@#^tZf{D&kz6RqsK`NI8_UhHya%mP(niiHH0f|8wkYXDAT!x!_I?13 zdgsuIQW*7&9O$D@{WWR@!1N7-*KQX!-)Zxjj!-Kx{Z!8kREr;r|0oj!8ew^!r=SqEm*ywzk9Y=3bwaJT z9-d%ue9sC=Y zJ*Qd0$DR_2sG^D;bn?JJq75EW{LC+G$^)oSZ$VLky0_W7jd8Gi2?N-8};$7Ljl+s8#pEQWaHq zyT?$C9#HP6wR>_mw6DBBEo0V8O`>f4fUGSg`uV+9(z(e_(<0!;QLB%;S_3z}uNf10 zTi)WqiE^i^hGQHGO-NcOu&YEi>>6p4lVhVQJ&8&(1kSu%n(Z&T6L4$tF>4@$t3PBX zs2OZhil-!0X*F-9`ba&?H6CxbD|>J-`HI4x&Z2L}VlP1)-3dI6wPU|cEA}qhz}OK? zMwK*maoOnN;`whM%2QLj7oV@zMi)8fcYM`@pwzRv&5#GSh5r9o02eQj*Nl)rl>l80 z`u!lBpJ{Kgx*-%|D#w-8JuHTt-7`D9rtqm6-@MwsSv$4*f%e|`YH&r8p+qEdi0bZ0 zbZvKPMXaVST;G!@p{Wq{?SF!3apdsV{UcMsa`PEM%Vz?P^eU{W3G8GV9cBRDJe#99 zMNvmR7Nk0C5iguF5+d^QHu9PIiD?lr4P#ynyC!PpqP=|aNj2hmRoVYhJBnvrewhqN zrf)wvKOr()&aRAZduu;P%X_wlv7wEiL?Dy|%HS9g9nLyD>$d6iH;Rv`3cNEH?bP&o zu(1uN?TDD!jj*$EE>8^~I61D`De7@=G zgP=ta)co;NCEGtXFV;k)BgoyC#@L39t-^j1xnH=Yr-2*z@-BkV58x;Y1f9#jo}PU#+mT<*c@nTh!LFi%yIQpyl2cWh8dmyY?S<`h;&%MXjCkC@M-w;Te5-Dx zwH3tdY;20()|HzPkzcfb`%vEDTz3+p05oGRELk*yKnN0zjrD&&6a9E*5*0w)ir+^b zA@c&YWx!i)>m1+FBjBswihm#wIMQ}ChB03-F-$$w0qHGbaz|}nZHF;>zZA-7DU8_D z-fZ7BwIEggZpIbyZE+UYJ4>Hb@@v0;%3-&1t^O(ZmbGFOMd`=s7`*+3w~b3j&wM>& zGKv>6Y`C4aTimxeeg((hwrjSXBzt*$xXi8Q_Mo1-@F(5eM-o4H+C%>-zMMK1^WNf* zY3wM)x06n2d3WlPG*&y|$j6C00YY`^UPTam7)R03fS0otv>sly@^sl8+DOM?6vdC= zC~^U;Y*@)OuS$rov*Ne-?X3iV5uI$MKcCRK2D2$oK~Ik#9to2^5U#7FtfI2zX!N`b zVs);>Qt0S~wQ7`YCOxZ|Y4mRYrtUX9m@@0HJfEp#R(eKar;yIB(DB31nGr?p<*u(1 z_6Hw?7<=)Q;68{TQqR63T4dplpNDS{e(*gdsE=}4l1E^LDdP3CNGe$4GKdE0GA*$c z(gkU0(*D@WOtVhsHOEydHnseMxC*c3--Pu2hYqFhOK^GiopjHBjT*+tj-sXLJNmSP z1yef(oEi+Nkwa*5rLgIJy1_ymD%3n3RFsa(-IYq8`p;7^$QtMJ>{~~i`wyfvM)yCT z6M`Ix5EBZ^Hpf&3^rzNF?yW0x`YEJBXZdANNEI6mqxx@*6Naqh{f7=F@0+2u%I-wv z8}#{65&GO*U1TDTzu~?+>eTQL%l(1bZcQ%qcvJFSF$xW2q3(TLwNG#@IPSzXEhkqW zt>jMFucG#W?>zn@{$nmb59CJm0M<+`;lb%@*;Vor6MlcOE2@fLk%jy7tljolSj43peE z{4_f#Ar5iHi!eF^I?7{Ek6o2~A-Q^m!r(zigO(R|ON)Sx}sQhPUME4DlBV~nI?PnXG5v*`N_tT=29KcxiGo? z=HpLp#Ioc$IWX;K$o~i?bqU?!)K$FfaAg&V)ISf{zHac#?Sx)u*VjClr{1wUew}i- z;p%o2d2#&(t21E5vcR6*3BB>_1EY8;6GgzGQQGRDxx0Y)U(~00Fr|I$v)|zv2!gjh zpz8LGt->yCFfxsVynOQqKVI=;a&Xa1&bpQh(l-dgJj_B6f|PJ!Udeq6YRSIQpGEwH zh#;&DXf?U6>^#wU$ul>#saLYMX+%mu%}66G41Qw(@3vRJf(`PYG#cmV)swuCB6!=F)lKG{~%Ag6h^@W;? zGnglNbn40Ll4tmb#r=nD5tXh1zJHh|PlHrh>X2}Eam#=b!H_`P}BH9u4o5BS-ARLb8+S(?U2`_>PfL9 z)z==>>$MdPb-K#X^N=njPTiTBu9=o@*Nd@0pZM;4dzNjXK)}ZkZ-}B; z!j>{P@TzawoW|HZ49lV8I6Vi$)^cxI9d62Ro8Uixeb$S*k%#Ww=n&RsviKDHXzXN1 zLd?l4^FlLqT6vN^L?y_*ujxkw{2Lsmkxo0g0Ttvco2sY!SQWb96kCXjwRN0u>mV;D zG1C>RH>VVZ$al(=sdlR>Eih1(X0dU3R7>aDaW4pkO zQEfRGKC;leK@wULi*OIsx;}#2dw3HYUO@@B>!!1zk66Ow80IMVK2?}&S?13w62>{ zje6;9RZkD{oF>oSn zldn|{QE4dw=`2NyeatV8Q9J8HIm8aFj~J|8tgfwjQu|YV(qPTvL?RcM#$_&Ads z$Jd=ERnnAp0-vs^8AGUB6&@!MHl*OR%(^r3SGKLU`?#@S@$cMdRjy1)Q)aR5sECgSZgZVd`|lP% zI*n9l`C@-U1ESKu__??UrfK;L%{Ux&fPJlC|ic8iga2m+`ZT6z_9xQ!Op<%*_#?7t8si| znzZ+NKAQiKYcXtV;|uy4fr9070c zJD)DxyPr1_tM_9@=9qW32vg)|n4=}oa~$52N1pFCo0jyk7u{Sh-Q}+Y%@X7(;`^yhB2D)^^kbse#B>Z}7-#yh>kX8{uHc1Let#)7G&0>T)1)7-S6FpH z-mtLXjCh?w8Lxm0CCGcF7G@1?%gWp~q<;-X=9Y!-t*s5+b7~hEnNz0Q176);*x|;w z$o+>7Med7(`}F-9&MGUdu>Q>TGF2~(3> z&RBz0r%SaViJ=&sO|hEXsS<@V$D16cYs~63c~iUc4k8V~jL8`*GVM`3|@QrZQ=U!-59C_QF|180NLT;A^LE>O@s~-(#Hl-y+8m?&5d-GZh!SN)f)@hU% zMe71`BOFQc#9?Od?cKHs_FNpl!Df<^Ek9q6z&NblpZ7G;x8e9Dc%7qS0D6w&Y7CQL zm>S3DZqdH{4f7!1m8E<-i_ZM|>|U|tk|NVtnqS=jz0icQ)z}v151SxJ&Hpk4+l{G3 zgPP}QP`OK6AD0qeUv9RILk729)#LGMH)hUh&V9FfAqClyD{AN!*6=DbU#misB2`s3 ztESN%hZuF0bq$&dL;ZtmAr*|Y$>;L{0RRNQ*u<+o0Ne?Rr6Qrasc(FoykT^n(RK z!_kg}7^5ul;nZu_3S)bM`OfK@eCq0j6nK|gRI!NF#qY@aS`(TSp(>A#(Nw$RLX9bQ zRd>eB{l6_dXKJD?Lgn69v0*AqT+N`7tDAa(%KyCP8K3ii^M9nt2|GyLTQOV>5qs3- zvWbJ>4d%3;lm@*;7@~J_6A66W5LmM736aJ zzz+KfdSCr_fWr*qyL2C($&wv~os- zGoda%vMbFXD%2S{0G$G)N^6Ok{Gf2cz3f-?P0T0g+Z& zO=LsEjAGA>Z0GvQxziVQmH$YaG2^<^bml0%Yo;`T0b#@h6qnKf?;C zNezL)%0%V_i&GdFq)GxOEL-ukg%Q=&5ew7Of+{sBlm>@O4RZE`6&4wLoKCA&UC3$v zOq@J5DnC+J850#=RuE~CC@pJYLJgaS+H3yk7YA++$c>5)D%dWO?E0g|j?&nK8t4Oa zQcLhBhFjf1ZG8;uxh*0teI>1X#MbH}BIt4ifqP%P7{t7^!51TkVT86iHZ>|VxZ%u> zUAe9GZ)b-y;H#PQDYVI5uO$+NS|U~RX9u*o)=}5-BSXQV1A}T z?GzG)JoKCi5#a2;)pk<;jal874O8C|B?KE;*+n8%31i6w2@!fjHF%WwT;+pNg*7%|A>rrVoTMkF!hR|6qKM?K5(p}5DpS`bot1e7<=!Fa)`EntuEFOH zj@%q4rMTxci@+pwH)edW>l?PzFaW(rQf8SAeFAqyZ+3pL(%~k8A@Yfz^~#*jbp2Xu z6Vn$FYJ_MsS->GW8otIjI4^!~ae^h_-00y5?lGAuZ2>grApB)VWDkKWAb-GL!&0=* z+piPn9{6-T@up2Gql#Qn^WglPC6L};kdBSU?ub=ZgG;I#;K6{f~y8Rbt!3EqiCw51NLW&xU)$rO#U(+xplydA&6bJsSM& zm<^eB$eOZwtLqvBqjY7= zUA@+?)mq9m&Q+{Z%WZz+0m8h+Q3y@D`bAK%UHp^(M37}c2xKD?dNZp88q83O|K8jX9Wc{`QKGWX>Oazn6=}i89|rBSXcQ6IdXD>?EW`s zAIf3bXj{nMb-41TBJLQEcazIK9s8cy57sa?4Le>BIhHiy;a{X-VJsSd{mPbq>kC*} zNtA;6fB6_%&!@4%Fxo{v%bRq(r)FbE&oIu$hdX*U*5p6#&*$H?g{Qd=1<=rt;Xl>t zKZiq5TEL++mwvBKJE>7mYPAz;&BPPwhthzzDbjNRB_bqQK7l9hafK&tRhKo6AYtI+ zFWDb66*_87SVY1M45M4o@(-8qN7s?bQ`5!r>A~{f_~==bzNYRaTi_A!&g|$zaBn-g zO9%!6q68Qh<}OO!B1}%ek~avFV~ok$V#eMC5TR;&e!GI%GN>n(G#Z_uDV( z*8K4X023d$@rSYc+-}*O=W}$g=@J>Q$;3-;d~K}8G8jM3#4c_e;AU#<*owMSwi3ph zF!6;OnX$&*dA%maJ1~*CQPEiYbur$s8-L%@TYSoQ2{o>J83F`BVbx*L5I_|uetVdM zoHa1Ec>wTlB{>g->mtF1|Awp?{zw4efFN4|;zLyRp*+$&Vrh)HERV_K@`QX_z9ZiS z_wJDaKR!x{pDCBR`;)&Jp8`rsb>rF-K#h-?0dZ#>n-@9=XE#vwq}&k27CBK*umN$n zWyB2w42WKzQ94+o86;L^K(+cFV{BVkufc78$H){KfZ-uLr$WcLYJfz`J_rg1TT-vM zz(x3^0d<|g-y(5ELWQNYAm3faEaTWN#&rdFCU9YmnsCLmc1#Djnyz#QudL|dGc{qr z93w7t5l)?e!s7vOOX7iWvsiTh5lAEk~a|>;cthXHiYm z%}iu@GC4Xp zBy~GMUYkf2cT5qG2h&;wP^G-lPZzys^lw#HK2BLep;S7Q%}u=eQN{j^~O_X3f;SoaD zP-_+FmweXQxEu7l$~gVi&X*!jz`2us4Qg1f>_^dwLeaDD?*@l zV;(yrMb25Bwgb_$cW`kx+d@~Gs#X@~(mcJ)XrrBojVX|!pA<+oYQ>@^I64^iI-N7@ zTNgN=AVOSOWB_C%P;-*i4!{0|=SvYLgoVFpIEn~funQQ`LnpLt;2hTJ}*T0W-%yb(k0kv5`cM9B*!GD2Vi#;*yUrx(d@HoWKHCw?x_A5N>8w^}dn8AcyHF1?vt6A*A+t@k< z8U$S>eswT#NO~G)u_YUIK)ufPdVJN5!PeGLEr|^UYGV4QQsSfuD1(SnEKE?Z_Lt!z zO6XA@c{?9{sKEhq&P0SFG}^WHCX0FvcLZqSh~u5(c1zV1-76Sl?2cQIoNlb6IK{3P zI}7wUK|b3EmiVq&(L(D`;m*2!fIV~Vz$l{wX$Q>Kn;@b`gSjYVl`Z6CUFJLq0}LG# z?NO?(HQO~~ay6jz=9%qTsG#Do<_j>a9CsWbH*dau==oZGc(>QEs@CoCN&;P?h&CYVi~-BP|PBWm4Zc}OggBw*%$C_Mv{ zqU`SvRLB;2n#56nErD@5+jW|xA;*g(Sw=!!!WU%7Ie)FFZYFZay$S)zETRayR&gMZ zis^=TkR|hqG^SlaVsbwaQhmDco@WM1`R>Niv|a;L8*Z=T(CS7MSsD}MTBxGh;V<$< z9E|T%Npaw%l~R*asGxF*>L*&$qXxAm@|zA3c+f1u5jVQ-i8NPZaxm-zI#^5=yQ1$5 zon}2jP!?)?=&hr@1dugw2$6+-MVg8>0A?i zBOtyzeH+`Wo|2`e=|W#kIL0fgepy@C5*-oW5vT4l=xuvLbqggQRGw68oeC=e z-$L*}>I|z&DjB!2mZ@A-)h3XLWI-k>rppO1tDMsKaI15G#MI4oUNQ~|@~oPOBkoiM zi-nKaz!f$yuzh4A)^YP*JBr$@cj{xi{Rt_H>2C-mip?1bInrkWMvXR83LgtysFk4! zBI`b%?eBq>SF^kM-SOexbbmVRoi?MIWFNJ5v0HCM=7a(&QOn z4qGN(e~k275#oQ8Y&r@sw2MP=4ZQI93>(n&d}MC7bJ$K!#HO!dO<5zp8EW3Fn@&vr$tZj^?@ui;hOjO08(CV-L~ zkRKqo=8#&5%;o{b=Phab{Sby9RCu@^bAdU9yr>wq3yoPHP}b?>bWsnoPGcC+YEwv- zq*qMT@;s%mths^*4s}uPRZrlFxu;=7m~gPzLgse}RBw8jF0v@_T`Ho`B>ZjU4iLMvM3Y1vio$`)zt$s0 zSi3H*YQ6>a+8fWJE^pL_GmzOmuu5jTERx8z!Ygh};{=a90A9u7NCf1#)>x|5cEkQQ zt9rsuPoH7th3;gRg{YhfI>Q%@*Um6uhMnL-Ggy9{zsyvN3;-=?mna6~0g`JP?vr40Y zK+i0D8Ds2AvOao?#gf3r-}hcp_S$4)V$l49*!c(bvqm2V(3{`|n<(Pd|S5%lE&0>0xo}lW-6qCqg2tTXv!+FC)$lpi!x)g)Slrqc)FQl>N5zm&IYL|;p~cG z=iHkjJ4~NpYC0_)fdLchOvPs`A?*Vaq<%sm2ed>Li)sbXUW=wBJFRhu{5n#o+?2&! z%5;{3*dDwGXIPx`0_f*(?Jb6EKZR-cHG+T<-$~xZ?E%fLxh@cw>>It8hvcsagCr}o zJ2u&0knUyAw2UaWW;K*hWKQL~(ma>6S|Y)jI%5`M>{mwBTA*H)3c{tOyR%gNWjDZZ z4O1E+-M%8*1813olEwW>j)5*ZptiB%^ni{HkJgrqj#@Ue^MfjRT8S$qeCk~$+nltu zv0+I4B|no6S8qWQqt%8ID;~3^A&k&rN{*OAX&b64wQ5pzyuNn^`FRFzY9hqYgqIcF zL8tSK?61p22v8uCY*O$*P5wuqPfAc9xKaw;7hf^`ObV}2q7ncl_!0vZRkUD!z1;em zLj>Tg0gj*hQE{k+f=sAHP==*#z=qwJidjZhAUD!)vo!*6gG>#d_jgYNPD{BG5$ULE_+C|Q>k zT2U#y*j;M3+lGP+kbPpI!Io7o6`rvarMvQ-c{Xl!A|1IO%1a|+fcAH7HH!&7dJm8{ ziN+a3z|RStfA#NHr5MGMSLy@1DN{@$m1d9|3VqQWb*=_`F@y$O*OW5cFpzmi3^FU#jMWh9i$IB_5@~#ClC=r16ZcmBVqk#> z|3+X(vZmSyAXEq$Krv1xVUnvU%qDu8^j|_o=tPL}8spx8Pmtye$x#vCJ4u zw{9+IDQ!?-%^6s-Iv(sU39`*O^OJu9oS4lja?=eIF{4(IhXGeq9 za!_4~v0B|=$-D=jmC2q4-u6Kg4FZbSB_vZ|Zo2T!sbNv$yT-Q<_t#hR>BOP8slF-7 zMbxdQJSNGUiYw!c3A5NCp=&B;(KI;t!JiAQs$SEX36-~=fFC8(H(xHU;5!m*J5*=t zYMivg3H$SbUrKoYb=D#;A&m+4T&5^yPj|-~5lH$VRePOsj$_4p4W9(#=N}x1eMklB znCZvT+>!*e2oy6dJ)m5(8x~l_CeEN4l4w_Ohlh^F)fd1$yU4q9TlG6#PfA_S_M z0mE~4?YjO*-(T-(+>@h0@8P^HL#Jh3Wo!!(v0B)bl2pfd|Jl_@m+HWg$jUUKG!g^t zoKFz0%3;3kP%BU%tN;58I^;r_{^i^bW=uAq)8=nHvDvD>QZxWP@k7tg7v zXptqtI%+XJWynB+@#GNUhXD80RI{^+F3)FY$A`{dzcY=k(WVqVj_#3=e!8$vn~89(Xk(qIU~9aX@$9+TkO!65gzl ziVNhU-D5h)P{6h_5O}{vVM>dH#oDV}FqRTNMz_4ABLaQ04fO#>irrbTCW-Ltl;I22 z9C4-39(I`t>QqTZDRC@_xE|Ac$7s%+^JHvDIBv3A9XPrilO#H6b`87aPg$-$J30kay z$3t{q6xLkfg?j~~-J|@Ce0@s%=b$u-en|M$8pKy_&Wz5$;?X$8uULN2VMLuq3nGBT z6)4p$?o!#iOJBFF?y|!&^z;ZVJ+;O&_~NQHlGaMm7@2>NV*sa)OZ{R<%nl8ZHA^!a zA!-dD!vNa*H71|Y)vU)2{V1M#M=?Z|Nz?(No5ap=+zgxu08}6m(H7zeZ|{`5y`#F( zBa5p(xLAt%=$vx48f_0r(OQeKr9vQtHq$$l5XF#kV>%@IR^km%oSo!IfS_*5hgb^o zT~yj}n9F~fAFgwIV@v%EO3=z60Hivi4fM<|dbp@YST2npfK-A#U{Bb4mcG_D$mQJJ zgRk_2T@u^`G>-rf;I2Lt{YVHDvMdMK_=o{SAV8^VL|`Ed08Ykq+7u+IU{eA-+?x_P zf;J`5dAuoEr^-z!v`h>i%K=k$t}VCf5GgdiLxpyAIqURPtq#_>v6hO}w7RBqaZA!X zSZA@kOOvWB)+tk!`|@fUUWICO!O9RhGU|`D^gBxEY3YvPD3T~b)ym8|Tj5%Dea0jd zrs|m`z_2ZwX@slnUQQ`WjcT{}7L7U-I;0h$<2NuXj?*le&qk6SStw1q4@#$~i%mzl z#Uboju|*GuYt(9(!OXBi5?MK{%JE{DXAiZgm#9z(>+KY^k4aXd6Z3Nb$xc!`d}<*Y3YNGK5Cs^H z_?Qg}Q(}r(@h-XSs81Yo+Z9t? zy0Wj4xHICwiZ-29$NS=+?u!3=5_vcRc59YzEBe2mIOzxK;9gpcI!kR$GyLyMk;^y`FOLU8Cx9(9?hhhh=&wz1(SdtdTv|d None: @@ -767,7 +767,7 @@ def test_pixel_food_pause_and_reduced_motion(self) -> None: wait_for_condition(page, "before => document.querySelector('[data-pond-state]')?.dataset.fishPositions !== before", arg=positions) x, y = find_open_water(page, pond) before = int(pond.get_attribute("data-food-dropped-count")) - page.mouse.click(x, y, button="right") + page.mouse.click(x, y) wait_for_condition(page, "before => Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > before", arg=before) self.assertGreater(int(pond.get_attribute("data-food-count")), 0) page.evaluate("""() => { @@ -1029,6 +1029,21 @@ def test_copyright_year_hydrates_when_the_visitors_clock_differs(self) -> None: expect(page.locator("footer")).to_contain_text(expected) self.assertEqual(issues, []) + def test_display_font_loads_under_the_production_security_policy(self) -> None: + browser = self.playwright.chromium.launch() + self.addCleanup(browser.close) + page = browser.new_page() + issues = [] + page.on("console", lambda message: record_console_issue(issues, message)) + page.goto(self.base_url, wait_until="networkidle") + loaded = page.evaluate("""async () => { + const faces = await document.fonts.load('46px "Instrument Serif"', 'Lu'); + return faces.length === 1 && faces[0].status === 'loaded'; + }""") + self.assertTrue(loaded) + self.assertIn('Instrument Serif', page.locator('#hero-title').evaluate('element => getComputedStyle(element).fontFamily')) + self.assertEqual(issues, []) + def test_homepage_is_responsive_and_routes_featured_work(self) -> None: for viewport_name, viewport in ( ("desktop", {"width": 1440, "height": 900}), @@ -1046,34 +1061,18 @@ def test_homepage_is_responsive_and_routes_featured_work(self) -> None: ).is_visible() ) expect(page.locator("#hero-title")).to_have_css("clip-path", "none") + cards = page.locator("#work article") + self.assertEqual(cards.count(), 4) for project in ("Rakazo", "linux-sonar", "HomeBot", "ConnectorMap"): - self.assertTrue( - page.get_by_role("button", name=re.compile(project, re.I)).is_visible() - ) - - wait_for_condition( - page, - "document.querySelector('[data-pond-state]')?.dataset.pondState === 'running'" - ) - impact_count = page.locator("[data-pond-state]").get_attribute( - "data-primary-impact-count" - ) - self.assertTrue( - page.get_by_role("group", name="Featured projects").is_visible() - ) - page.get_by_role("button", name=re.compile("Rakazo", re.I)).click() - self.assertEqual( - page.locator("[data-pond-state]").get_attribute( - "data-primary-impact-count" - ), - impact_count, - ) - proof = page.get_by_text("4 highlighted merged PRs", exact=True) - proof.wait_for(state="visible") - self.assertTrue(proof.is_visible()) - self.assertTrue( - page.get_by_role("link", name="Merged work").is_visible() - ) + expect(cards.get_by_role("heading", name=project, exact=True)).to_be_visible() + wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.pondState === 'running'") + impact_count = page.locator("[data-pond-state]").get_attribute("data-primary-impact-count") + cards.first.scroll_into_view_if_needed() + cards.first.get_by_role("heading").click() + self.assertEqual(page.locator("[data-pond-state]").get_attribute("data-primary-impact-count"), impact_count) + expect(cards.first.locator("[class*=projectFigure] strong")).to_have_text("4") + expect(cards.first.get_by_text("highlighted merged PRs upstream", exact=True)).to_be_visible() + expect(cards.first.get_by_role("link", name="Merged work")).to_be_visible() self.assertLessEqual( page.evaluate("document.documentElement.scrollWidth"), viewport["width"], @@ -1087,46 +1086,28 @@ def test_homepage_is_responsive_and_routes_featured_work(self) -> None: ) browser.close() - def test_project_marker_is_centered_for_every_selection(self) -> None: + def test_project_grid_shows_every_card_without_selection(self) -> None: browser = self.playwright.chromium.launch() self.addCleanup(browser.close) for width, height in ((1440, 900), (390, 844), (320, 800)): with self.subTest(width=width): page = browser.new_page(viewport={"width": width, "height": height}) page.goto(self.base_url, wait_until="networkidle") - buttons = page.get_by_role("group", name="Featured projects").get_by_role("button") - for index in range(4): - button = buttons.nth(index) - button.click() - page.wait_for_timeout(400) - self.assertEqual(button.get_attribute("aria-pressed"), "true") - centers = button.evaluate("""async button => { - const marker = button.querySelector('i'); - let previous = null; - let stable = 0; - const deadline = performance.now() + 5000; - while (performance.now() < deadline) { - await new Promise(requestAnimationFrame); - const bounds = button.getBoundingClientRect(); - const dot = marker.getBoundingClientRect(); - const centers = [bounds.y + bounds.height / 2, dot.y + dot.height / 2]; - stable = previous && centers.every((value, index) => Math.abs(value - previous[index]) < 0.05) ? stable + 1 : 0; - if (stable >= 3) return centers; - previous = centers; - } - throw new Error('Project marker did not settle'); - }""") - self.assertAlmostEqual(centers[0], centers[1], delta=0.75) - if width <= 390: - heading = page.locator("#work article h3") - expect(heading).to_have_text(button.locator("strong").inner_text()) - heading.scroll_into_view_if_needed() - self.assertTrue(heading.evaluate("""element => { - const range = document.createRange(); - range.selectNodeContents(element); - const lines = [...range.getClientRects()]; - return lines.length === 1 && lines[0].width <= element.clientWidth; - }"""), f"Project heading wraps at {width}px") + cards = page.locator("#work article") + self.assertEqual(cards.count(), 4) + self.assertEqual(page.locator("#work [aria-pressed]").count(), 0) + for index, project in enumerate(("Rakazo", "linux-sonar", "HomeBot", "ConnectorMap")): + card = cards.nth(index) + heading = card.get_by_role("heading", name=project, exact=True) + card.scroll_into_view_if_needed() + expect(heading).to_be_visible() + expect(card.get_by_role("link", name=f"View project {project}", exact=True)).to_be_visible() + self.assertTrue(heading.evaluate("""element => { + const range = document.createRange(); + range.selectNodeContents(element); + return [...range.getClientRects()].every(line => line.width <= element.clientWidth + 1); + }"""), f"Project heading overflows at {width}px") + page.screenshot(path=SCREENSHOTS / f"project-grid-{width}.png", full_page=True) page.close() def test_mobile_navigation_hero_viewport_and_system_color_modes(self) -> None: @@ -1268,9 +1249,9 @@ def test_homepage_preserves_layout_and_world_state_across_viewport_matrix(self) const heading = document.querySelector('#hero-title').getBoundingClientRect(); const header = document.querySelector('header').getBoundingClientRect(); const pond = document.querySelector('[data-renderer]').getBoundingClientRect(); - const buttons = [...document.querySelectorAll('[aria-label="Featured projects"] button')] + const buttons = [...document.querySelectorAll('#work article [class*="projectActions"] a:first-child')] .map(element => element.getBoundingClientRect()); - const projectNames = [...document.querySelectorAll('[aria-label="Featured projects"] strong')]; + const projectNames = [...document.querySelectorAll('#work article h3')]; const aboutCards = [...document.querySelectorAll('#about > article, #about > div:last-child')]; const layoutWidth = document.documentElement.clientWidth; const fish = document.querySelector('[data-pond-state]').dataset.fishPositions @@ -1281,11 +1262,16 @@ def test_homepage_preserves_layout_and_world_state_across_viewport_matrix(self) headerInside: header.left >= -1 && header.right <= width + 1, pondFits: Math.abs(pond.width - width) < 1 && Math.abs(pond.height - height) < 1, projectTargets: buttons.length === 4 && buttons.every(button => button.width >= 44 && button.height >= 44), - projectDistribution: width <= 430 || (buttons.length === 4 && Math.abs(buttons[3].right - document.querySelector('[aria-label="Featured projects"]').getBoundingClientRect().right) < 2), + projectDistribution: (() => { + const cards = [...document.querySelectorAll('#work article')].map(card => card.getBoundingClientRect()); + return cards.length === 4 && (width <= 900 + ? cards.every((card, index) => index === 0 || card.top >= cards[index - 1].bottom) + : Math.abs(cards[0].top - cards[1].top) < 1 && cards[1].left >= cards[0].right && cards[2].top >= cards[0].bottom); + })(), projectNamesFit: width > 430 || projectNames.every(element => { const range = document.createRange(); range.selectNodeContents(element); - return range.getBoundingClientRect().width <= element.getBoundingClientRect().width + 1; + return [...range.getClientRects()].every(line => line.width <= element.getBoundingClientRect().width + 1); }), aboutCardsCentered: width > 430 || aboutCards.every(element => { const bounds = element.getBoundingClientRect(); @@ -1432,7 +1418,7 @@ def test_loading_shows_skeleton_then_fades_in_the_live_ecosystem(self) -> None: self.assertEqual(host.evaluate("element => getComputedStyle(element).opacity"), "1") browser.close() - def test_hero_copy_does_not_block_pointer_water_reaction(self) -> None: + def test_hero_copy_is_selectable_and_blocks_pond_input(self) -> None: browser = self.playwright.chromium.launch() page = self.pond_page(browser, viewport={"width": 1280, "height": 800}) page.goto(f"{self.base_url}/?pond-seed=e2e-hero-pointer", wait_until="networkidle") @@ -1453,7 +1439,12 @@ def test_hero_copy_does_not_block_pointer_water_reaction(self) -> None: box["y"] + box["height"] * vertical, ) responsive |= pond.get_attribute("data-food-affordance") == "true" - self.assertTrue(responsive) + self.assertFalse(responsive) + self.assertEqual(page.locator('#hero-title').evaluate('element => getComputedStyle(element).userSelect'), 'auto') + before_food = pond.get_attribute("data-food-dropped-count") + page.get_by_role("heading", name="I make stubborn software behave.").click() + page.wait_for_timeout(200) + self.assertEqual(pond.get_attribute("data-food-dropped-count"), before_food) action = page.get_by_role("link", name="Things I’ve made").bounding_box() page.mouse.move( @@ -1497,12 +1488,12 @@ def test_navigation_and_bordered_controls_keep_their_visual_hierarchy(self) -> N navigation.evaluate_all( "elements => elements.map(element => [element.textContent.trim(), element.getAttribute('href')])" ), - [["Work", "#work"], ["About", "#about"], ["Contact", "#contact"]], + [["Work", "#work"], ["More", "#about"], ["Contact", "#contact"]], ) expect(hero.get_by_role("img", name="Lu's illustrated avatar wearing a pink cap")).to_be_visible() expect(hero.get_by_role("heading", level=1)).to_have_text("Hi, I’m Lu.") expect(page.locator("header a[href='#top']")).to_be_visible() - category_labels = page.locator("[aria-label='Featured projects'] button small") + category_labels = page.locator("#work article [class*=projectEyebrow]") self.assertEqual(category_labels.count(), 4) self.assertTrue( category_labels.evaluate_all( @@ -1525,21 +1516,19 @@ def test_navigation_and_bordered_controls_keep_their_visual_hierarchy(self) -> N } def assert_homebot_image_fits(panel) -> None: - illustration = panel.locator('svg[aria-hidden="true"]').first + illustration = panel.locator('[class*=projectIllustration] > svg') expect(illustration).to_be_visible() self.assertGreater(illustration.bounding_box()["width"], 100) self.assertEqual(panel.locator("video, img").count(), 0) for project, href in project_hrefs.items(): - button = page.get_by_role("button", name=re.compile(project, re.I)) - button.click() - self.assertEqual(button.get_attribute("aria-pressed"), "true") panel = page.locator("article").filter( has=page.get_by_role("heading", name=project, exact=True) ) + panel.scroll_into_view_if_needed() panel.wait_for(state="visible") self.assertEqual( - panel.get_by_role("link", name="View project", exact=True).get_attribute( + panel.get_by_role("link", name=f"View project {project}", exact=True).get_attribute( "href" ), href, @@ -1554,18 +1543,16 @@ def assert_homebot_image_fits(panel) -> None: )).to_be_visible() for tag in ("Read + act", "Marked inferences", "Source links"): expect(panel.locator("[class*='proofRow']").get_by_text(tag, exact=True)).to_be_visible() - expect(panel.locator('svg[aria-hidden="true"]').first).to_be_visible() - link = panel.get_by_role("link", name="View project", exact=True) + expect(panel.locator('[class*=projectIllustration] > svg')).to_be_visible() + link = panel.get_by_role("link", name=f"View project {project}", exact=True) expect(link).to_have_attribute("target", "_blank") expect(link).to_have_attribute("rel", "noreferrer") page.set_viewport_size({"width": 390, "height": 844}) - homebot_button = page.get_by_role("button", name=re.compile("HomeBot", re.I)) - homebot_button.click() - self.assertEqual(homebot_button.get_attribute("aria-pressed"), "true") homebot_panel = page.locator("article").filter( has=page.get_by_role("heading", name="HomeBot", exact=True) ) + homebot_panel.scroll_into_view_if_needed() homebot_panel.wait_for(state="visible") assert_homebot_image_fits(homebot_panel) browser.close() @@ -1586,8 +1573,9 @@ def test_pointer_stirs_the_pond(self) -> None: self.assertEqual( page.evaluate("getComputedStyle(document.querySelector('#top')).userSelect"), - "none", + "auto", ) + self.assertEqual(page.locator('[data-pond-state]').evaluate('element => getComputedStyle(element).userSelect'), 'none') self.assertEqual(page.get_by_text("Touch the water", exact=True).count(), 0) pond = page.locator("[data-pond-state]") @@ -1716,15 +1704,15 @@ def test_food_drop_reservation_and_feeding(self) -> None: }, { once: true })""" ) page.mouse.click(fish_x, fish_y, button="right") + self.assertEqual(page.locator("html").get_attribute("data-pond-context-prevented"), "false") + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before) + page.mouse.click(fish_x, fish_y) wait_for_condition( page, "initial => Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > initial", arg=food_before, ) - self.assertEqual( - page.locator("html").get_attribute("data-pond-context-prevented"), - "true", - ) + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before + 1) camera_x, camera_y, scale_x, scale_y = ( float(value) for value in pond.get_attribute("data-pond-camera").split(",") ) @@ -1825,7 +1813,7 @@ def test_reduced_motion_preserves_the_ecosystem_at_low_speed(self) -> None: for point in pond.get_attribute("data-fish-positions").split(";") ] fish_x, fish_y = find_open_water(page, pond, fish_points) - page.mouse.click(fish_x, fish_y, button="right") + page.mouse.click(fish_x, fish_y) wait_for_condition( page, "Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > 0" @@ -1852,52 +1840,14 @@ def test_mobile_touch_disturbs_the_shared_pond(self) -> None: for point in pond.get_attribute("data-fish-positions").split(";") ] fish_x, fish_y = find_open_water(page, pond, fish_points) - page.evaluate("""() => { - window.__pondTouchEvents = []; - for (const type of ['pointerdown', 'pointerup', 'pointercancel']) { - document.addEventListener(type, event => { - if (event.pointerType === 'touch') window.__pondTouchEvents.push({ - type, time: performance.now(), x: event.clientX, y: event.clientY, - target: event.target?.tagName, - }); - }, true); - } - }""") impacts_before = int(pond.get_attribute("data-primary-impact-count")) + food_before = int(pond.get_attribute("data-food-dropped-count")) page.touchscreen.tap(fish_x, fish_y) - wait_for_condition( - page, - "document.querySelector('[data-pond-state]')?.dataset.touchGesture === 'single-tap-impact'" - ) - self.assertEqual( - int(pond.get_attribute("data-primary-impact-count")), - impacts_before + 1, - ) + wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.touchGesture === 'tap-food'") + wait_for_condition(page, "before => Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount) === before + 1", arg=food_before) + self.assertEqual(int(pond.get_attribute("data-primary-impact-count")), impacts_before + 1) self.assertGreater(int(pond.get_attribute("data-ring-count")), 0) - self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), 0) - impacts_before_food = int(pond.get_attribute("data-primary-impact-count")) - touch_session = context.new_cdp_session(page) - - def double_tap(x, y): - touch_session.send("Input.synthesizeTapGesture", { - "x": x, "y": y, "duration": 100, - "tapCount": 2, "gestureSourceType": "touch", - }) - - double_tap(fish_x, fish_y) - try: - wait_for_condition( - page, - "Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > 0" - ) - except PlaywrightTimeoutError: - detail = pond.evaluate( - "element => ({ gesture: element.dataset.touchGesture, requested: element.dataset.foodRequestedAt, frame: element.dataset.frame, state: element.dataset.pondState })" - ) - detail["touch_events"] = page.evaluate("window.__pondTouchEvents") - context.close() - browser.close() - self.fail(f"Double-tap did not commit food: {detail}") + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before + 1) camera_x, camera_y, scale_x, scale_y = ( float(value) for value in pond.get_attribute("data-pond-camera").split(",") ) @@ -1913,16 +1863,19 @@ def double_tap(x, y): page.wait_for_timeout(500) self.assertEqual( int(pond.get_attribute("data-primary-impact-count")), - impacts_before_food, + impacts_before + 1, ) dropped_count = int(pond.get_attribute("data-food-dropped-count")) - project_button = page.get_by_role("button", name=re.compile("Rakazo", re.I)) - project_button.scroll_into_view_if_needed() - box = project_button.bounding_box() - double_tap(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) + project_link = page.get_by_role("link", name="Things I’ve made", exact=True) + project_link.scroll_into_view_if_needed() + box = project_link.bounding_box() + page.touchscreen.tap(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) page.wait_for_timeout(500) self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), dropped_count) + page.evaluate("window.scrollTo(0, 0)") + fish_x, fish_y = find_open_water(page, pond) + def pointer_sequence(events): page.evaluate( """({ x, y, events }) => { @@ -1970,7 +1923,7 @@ def pointer_sequence(events): self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), dropped_count) self.assertEqual( int(pond.get_attribute("data-primary-impact-count")), - impacts_before_food, + impacts_before + 1, ) self.assertGreaterEqual(int(pond.get_attribute("data-fish-count")), 8) context.close() diff --git a/tests/browser/issues_test.py b/tests/browser/issues_test.py index c45154c..fead406 100644 --- a/tests/browser/issues_test.py +++ b/tests/browser/issues_test.py @@ -24,7 +24,7 @@ def test_actions_captions_privacy_and_immediate_navigation(self): self.assertGreaterEqual(link.evaluate('e=>parseFloat(getComputedStyle(e).fontSize)'),14) self.assertGreaterEqual(link.bounding_box()['height'],44) expect(page.locator('footer').get_by_role('link',name='Privacy')).to_have_attribute('href','/privacy') - for section in ('Work','About','Contact'): + for section in ('Work','More','Contact'): page.get_by_role('navigation',name='Portfolio navigation').get_by_role('link',name=section,exact=True).click() expect(page.locator('header [aria-current="location"]')).to_have_text(section) self.assertEqual(page.locator('#contact').evaluate('e=>getComputedStyle(e).opacity'),'1') From 5a28bc85d9918db786d7a61f12cdf96abb022d7b Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:40:22 +0000 Subject: [PATCH 4/6] Revert "Restore keyboard and screen reader feeding without the visible button" This reverts commit 5eb53e3d214025f4d71eac80866fa6dc78dbcb41. --- components/concepts/signal-desk/pond-input.ts | 29 +--------------- .../signal-desk/signal-desk.module.css | 33 ------------------- .../concepts/signal-desk/signal-desk.tsx | 12 +------ 3 files changed, 2 insertions(+), 72 deletions(-) diff --git a/components/concepts/signal-desk/pond-input.ts b/components/concepts/signal-desk/pond-input.ts index 3e565b6..d4ffd2f 100644 --- a/components/concepts/signal-desk/pond-input.ts +++ b/components/concepts/signal-desk/pond-input.ts @@ -16,8 +16,7 @@ function isCompletedTap(touch: { moved: boolean; started: number }, now: number, } // A single click or tap on open water splashes and drops food. The browser's -// context menu is never intercepted. Keyboard users feed through the -// "pond-feed" event (see feedFromControl). +// context menu is never intercepted. export function installPondInput(options: PondInputOptions) { const { interaction } = options; @@ -102,30 +101,6 @@ export function installPondInput(options: PondInputOptions) { if (activeTouch?.id === event.pointerId) activeTouch = null; }; - // Keyboard and screen reader feeding. The pond surface is aria-hidden, so a - // visually hidden "Feed fish" control dispatches a cancelable "pond-feed" - // event here. Search a centre-out grid for open water that no page content - // ([data-pond-obscures]) covers and drop food at the first hit. Calling - // preventDefault tells the dispatcher that food was dropped. - const feedCandidates = [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]; - const isObscured = (screen: PondPoint) => - [...document.querySelectorAll("[data-pond-obscures]")].some(element => { - const box = element.getBoundingClientRect(); - return screen.x >= box.left && screen.x <= box.right && screen.y >= box.top && screen.y <= box.bottom; - }); - - const feedFromControl = (event: Event) => { - for (const y of feedCandidates) { - for (const x of feedCandidates) { - const screen = { x: innerWidth * x, y: innerHeight * y }; - if (!isObscured(screen) && ownsPoint(screen) && dropFood(screen)) { - event.preventDefault(); - return; - } - } - } - }; - const resize = () => { activeTouch = null; }; @@ -134,7 +109,6 @@ export function installPondInput(options: PondInputOptions) { interaction.addEventListener("pointerdown", press, { passive: true }); interaction.addEventListener("pointerup", release, { passive: false }); interaction.addEventListener("pointercancel", cancel, { passive: true }); - interaction.addEventListener("pond-feed", feedFromControl); window.addEventListener("resize", resize, { passive: true }); return () => { @@ -142,7 +116,6 @@ export function installPondInput(options: PondInputOptions) { interaction.removeEventListener("pointerdown", press); interaction.removeEventListener("pointerup", release); interaction.removeEventListener("pointercancel", cancel); - interaction.removeEventListener("pond-feed", feedFromControl); window.removeEventListener("resize", resize); }; } diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css index a435c2d..fc8ea90 100644 --- a/components/concepts/signal-desk/signal-desk.module.css +++ b/components/concepts/signal-desk/signal-desk.module.css @@ -76,39 +76,6 @@ border:0; } -/* The keyboard and screen reader feed control: visually hidden like - .srOnly until it receives keyboard focus, then it appears as a pond - control chip (same pattern as the skip link). */ -.feedControl { - position:absolute; - width:1px; - height:1px; - padding:0; - margin:-1px; - overflow:hidden; - clip:rect(0,0,0,0); - white-space:nowrap; - border:0; -} - -.feedControl:focus-visible { - position:static; - width:auto; - height:auto; - min-height:44px; - padding:0 11px; - margin:0; - overflow:visible; - clip:auto; - white-space:normal; - border:1px solid var(--control-edge); - border-radius:2px; - background:var(--paper); - font-family:var(--font-space-mono),monospace; - font-size:13px; - cursor:pointer; -} - .header { position:fixed; z-index:30; diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx index 7df3cc7..d2637c7 100644 --- a/components/concepts/signal-desk/signal-desk.tsx +++ b/components/concepts/signal-desk/signal-desk.tsx @@ -215,7 +215,6 @@ export function SignalDesk() { return () => { window.removeEventListener("scroll", sync); window.removeEventListener("hashchange", sync); }; }, []); const [pondStatus, setPondStatus] = useState<{ mode: string; renderer: string }>({ mode: "ascii", renderer: "loading" }); - const [feedStatus, setFeedStatus] = useState(""); const entrance = ready && !reduced; useEffect(() => { @@ -231,7 +230,7 @@ export function SignalDesk() { return (
-

The decorative pond responds to pointer movement. Click or tap open water to feed the fish, or use the Feed fish button next to the pond view options to drop food with the keyboard.

+

The decorative pond responds to pointer movement. Click or tap open water to feed the fish.

@@ -358,14 +357,6 @@ export function SignalDesk() { ))}
- {/* Visually hidden: the pond surface is aria-hidden, so this is the - keyboard and screen reader route to feeding. Pointer users click - or tap the water instead. */} -
{pondStatus.mode !== pondView || pondStatus.renderer === "loading" ? `Loading ${pondView === "ascii" ? "ASCII" : "Pixel"} view…` @@ -373,7 +364,6 @@ export function SignalDesk() { ? "Pond unavailable. Try the other view." : `Current view: ${pondView === "ascii" ? "ASCII" : "Pixel"}`}
- {feedStatus} From 3c0d997df8b5603c2608585eda4edd54f75b12f2 Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:41:09 +0000 Subject: [PATCH 5/6] Verify pointer feeding without any Feed control --- tests/browser/issues_test.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/browser/issues_test.py b/tests/browser/issues_test.py index fead406..b6797b8 100644 --- a/tests/browser/issues_test.py +++ b/tests/browser/issues_test.py @@ -1,5 +1,5 @@ """Behaviour checks for the ready homepage issue batch.""" -from browser_test import BrowserTestCase, expect, wait_for_condition +from browser_test import BrowserTestCase, expect, wait_for_condition, find_open_water class HomepageIssueTests(BrowserTestCase): def test_skip_link_is_first_and_focuses_main(self): @@ -35,7 +35,7 @@ def test_actions_captions_privacy_and_immediate_navigation(self): self.assertEqual(page.locator('#contact').evaluate('e=>getComputedStyle(e).opacity'),'1') page.close() - def test_feed_button_keyboard_and_ascii_without_unused_rasters(self): + def test_pointer_feeding_without_feed_control_or_unused_ascii_rasters(self): browser = self.playwright.chromium.launch() self.addCleanup(browser.close) page = self.pond_page(browser) @@ -47,9 +47,11 @@ def test_feed_button_keyboard_and_ascii_without_unused_rasters(self): for mode in ('ASCII','Pixel'): page.get_by_label(mode,exact=True).check() wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.pondState==='running'") - feed=page.get_by_role('button',name='Feed fish',exact=True) - expect(feed).to_be_enabled() - before=int(page.locator('[data-pond-state]').get_attribute('data-food-dropped-count')) - feed.focus();page.keyboard.press('Enter') - wait_for_condition(page, "before=>Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount)>before",arg=before) - expect(page.get_by_role('status',name='').filter(has_text='Food dropped')).to_be_visible() + self.assertEqual(page.get_by_role('button',name='Feed fish',exact=True,include_hidden=True).count(),0) + self.assertEqual(page.locator('[class*="feedControl"], [class*="feedButton"]').count(),0) + pond=page.locator('[data-pond-state]') + before=int(pond.get_attribute('data-food-dropped-count')) + x,y=find_open_water(page,pond) + page.mouse.click(x,y) + wait_for_condition(page, "before=>Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount)===before+1",arg=before) + self.assertEqual(int(pond.get_attribute('data-food-dropped-count')),before+1) From cb0744df647c610ac353cbfbcd44021c7eff92eb Mon Sep 17 00:00:00 2001 From: luinbytes <42706009+luinbytes@users.noreply.github.com> Date: Thu, 12 Nov 2026 00:51:10 +0000 Subject: [PATCH 6/6] Center the project heading before testing pointer isolation --- tests/browser/browser_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/browser/browser_test.py b/tests/browser/browser_test.py index b6550d7..74d82d8 100644 --- a/tests/browser/browser_test.py +++ b/tests/browser/browser_test.py @@ -1067,8 +1067,9 @@ def test_homepage_is_responsive_and_routes_featured_work(self) -> None: expect(cards.get_by_role("heading", name=project, exact=True)).to_be_visible() wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.pondState === 'running'") impact_count = page.locator("[data-pond-state]").get_attribute("data-primary-impact-count") - cards.first.scroll_into_view_if_needed() - cards.first.get_by_role("heading").click() + heading = cards.first.get_by_role("heading") + heading.evaluate("element => element.scrollIntoView({ block: 'center', behavior: 'instant' })") + heading.click() self.assertEqual(page.locator("[data-pond-state]").get_attribute("data-primary-impact-count"), impact_count) expect(cards.first.locator("[class*=projectFigure] strong")).to_have_text("4") expect(cards.first.get_by_text("highlighted merged PRs upstream", exact=True)).to_be_visible()