Skip to content

Commit fe9d4c4

Browse files
authored
Merge branch 'main' into t3code/fix-windows-release-workflow
2 parents 9646074 + d28077e commit fe9d4c4

48 files changed

Lines changed: 2963 additions & 231 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Setup APT mirrors
2+
description: Configure Ubuntu package downloads with automatic mirror failover.
3+
runs:
4+
using: composite
5+
steps:
6+
- shell: bash
7+
run: |
8+
# Replace the existing Blacksmith mirror list as well as direct sources.
9+
printf '%s\tpriority:%s\n' \
10+
https://archive.ubuntu.com/ubuntu 1 \
11+
https://mirrors.edge.kernel.org/ubuntu 2 \
12+
https://mirror.math.princeton.edu/pub/ubuntu 3 \
13+
| sudo tee /etc/apt/blacksmith-ubuntu-mirrors.txt > /dev/null
14+
15+
# APT's mirror transport retries each file against the next server.
16+
sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) \
17+
-exec sed -i -E \
18+
's#https?://(([^/]+\.)?archive|security)\.ubuntu\.com/ubuntu/?#mirror+file:/etc/apt/blacksmith-ubuntu-mirrors.txt#g' {} +
19+
20+
# Move on to a fallback before an unreachable server exhausts the job.
21+
printf '%s\n' 'Acquire::http::Timeout "15";' 'Acquire::https::Timeout "15";' \
22+
| sudo tee /etc/apt/apt.conf.d/80-mirror-timeouts > /dev/null

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@ jobs:
5151
- name: Typecheck
5252
run: vpr typecheck
5353

54+
- uses: ./.github/actions/setup-apt-mirrors
55+
5456
- name: Install browser secret helper build libraries
55-
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
57+
run: |
58+
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
59+
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
5660
5761
- name: Build desktop pipeline
5862
run: vp run build:desktop
@@ -88,8 +92,12 @@ jobs:
8892
- name: Ensure Electron runtime is installed
8993
run: vp run --filter @t3tools/desktop ensure:electron
9094

95+
- uses: ./.github/actions/setup-apt-mirrors
96+
9197
- name: Install browser secret helper build libraries
92-
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
98+
run: |
99+
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
100+
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
93101
94102
- name: Test
95103
run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test

‎.github/workflows/release.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ jobs:
216216
- name: Typecheck
217217
run: vp run typecheck
218218

219+
- uses: ./.github/actions/setup-apt-mirrors
220+
219221
- name: Install browser secret helper build libraries
220222
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
221223

@@ -527,6 +529,9 @@ jobs:
527529
exit $code
528530
}
529531
532+
- uses: ./.github/actions/setup-apt-mirrors
533+
if: matrix.platform == 'linux'
534+
530535
- name: Install Linux desktop build libraries
531536
if: matrix.platform == 'linux'
532537
shell: bash
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2+
3+
import { startHomeMotion } from "./homeMotion";
4+
5+
class ElementStub extends EventTarget {
6+
properties = new Map<string, string>();
7+
style = { setProperty: (name: string, value: string) => this.properties.set(name, value) };
8+
children: ElementStub[] = [];
9+
scrollLeft = 0;
10+
scrollWidth = 1_200;
11+
clientWidth = 400;
12+
matches = () => false;
13+
contains = (target: EventTarget | null) =>
14+
target === this || (target instanceof ElementStub && this.children.includes(target));
15+
querySelectorAll = () => this.children;
16+
getBoundingClientRect = vi.fn(() => ({ left: 0, top: 0, width: 400, height: 600 }));
17+
scrollTo = vi.fn((options: ScrollToOptions) => {
18+
this.scrollLeft = options.left ?? this.scrollLeft;
19+
});
20+
}
21+
22+
let observers: ObserverStub[] = [];
23+
class ObserverStub {
24+
constructor(private readonly callback: IntersectionObserverCallback) {
25+
observers.push(this);
26+
}
27+
observe = vi.fn();
28+
disconnect = vi.fn();
29+
report(target: ElementStub, isIntersecting: boolean) {
30+
this.callback(
31+
[{ target, isIntersecting } as unknown as IntersectionObserverEntry],
32+
this as unknown as IntersectionObserver,
33+
);
34+
}
35+
}
36+
37+
let page = Object.assign(new EventTarget(), { visibilityState: "visible", activeElement: null });
38+
let viewport = new EventTarget();
39+
let reduced = Object.assign(new EventTarget(), { matches: false });
40+
let fine = Object.assign(new EventTarget(), { matches: true });
41+
let frames = new Map<number, FrameRequestCallback>();
42+
let dispose: (() => void) | undefined;
43+
44+
beforeEach(() => {
45+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
46+
observers = [];
47+
frames = new Map();
48+
page = Object.assign(new EventTarget(), { visibilityState: "visible", activeElement: null });
49+
viewport = new EventTarget();
50+
reduced = Object.assign(new EventTarget(), { matches: false });
51+
fine = Object.assign(new EventTarget(), { matches: true });
52+
vi.stubGlobal("document", page);
53+
vi.stubGlobal(
54+
"window",
55+
Object.assign(viewport, {
56+
matchMedia: (query: string) => (query.includes("reduced-motion") ? reduced : fine),
57+
}),
58+
);
59+
vi.stubGlobal("Node", ElementStub);
60+
vi.stubGlobal("IntersectionObserver", ObserverStub);
61+
let frameId = 0;
62+
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
63+
frames.set(++frameId, callback);
64+
return frameId;
65+
});
66+
vi.stubGlobal("cancelAnimationFrame", (id: number) => frames.delete(id));
67+
});
68+
69+
afterEach(() => {
70+
dispose?.();
71+
dispose = undefined;
72+
vi.useRealTimers();
73+
vi.unstubAllGlobals();
74+
});
75+
76+
function fixture() {
77+
const hero = new ElementStub();
78+
const field = new ElementStub();
79+
const mark = new ElementStub();
80+
const otherMark = new ElementStub();
81+
field.children = [mark, otherMark];
82+
const endorsements = new ElementStub();
83+
const caret = new ElementStub();
84+
dispose = startHomeMotion({ hero, field, endorsements, caret } as unknown as Parameters<
85+
typeof startHomeMotion
86+
>[0]);
87+
return { hero, field, mark, otherMark, endorsements, caret, observer: observers[0]! };
88+
}
89+
90+
function movePointer(hero: ElementStub, x = 400, y = 600) {
91+
hero.dispatchEvent(Object.assign(new Event("pointermove"), { clientX: x, clientY: y }));
92+
}
93+
94+
describe("homepage motion", () => {
95+
it("gates each mark and caret and batches pointer input into one frame", () => {
96+
const { hero, field, mark, otherMark, caret, observer } = fixture();
97+
expect(mark.properties.get("--home-motion-state")).toBe("paused");
98+
observer.report(mark, true);
99+
observer.report(caret, true);
100+
expect(mark.properties.get("--home-motion-state")).toBe("running");
101+
expect(otherMark.properties.get("--home-motion-state")).toBe("paused");
102+
expect(caret.properties.get("--home-motion-state")).toBe("running");
103+
104+
movePointer(hero, 100, 100);
105+
movePointer(hero);
106+
expect(frames.size).toBe(1);
107+
expect(hero.getBoundingClientRect).not.toHaveBeenCalled();
108+
const [id, callback] = [...frames][0]!;
109+
frames.delete(id);
110+
callback(0);
111+
expect(field.properties.get("--px")).toBe("18.0px");
112+
expect(field.properties.get("--py")).toBe("14.0px");
113+
114+
movePointer(hero);
115+
page.visibilityState = "hidden";
116+
page.dispatchEvent(new Event("visibilitychange"));
117+
expect(frames.size).toBe(0);
118+
expect(field.properties.get("--px")).toBe("0px");
119+
expect(mark.properties.get("--home-motion-state")).toBe("paused");
120+
expect(caret.properties.get("--home-motion-state")).toBe("paused");
121+
page.visibilityState = "visible";
122+
page.dispatchEvent(new Event("visibilitychange"));
123+
reduced.matches = true;
124+
reduced.dispatchEvent(new Event("change"));
125+
movePointer(hero);
126+
expect(frames.size).toBe(0);
127+
expect(mark.properties.get("--home-motion-state")).toBe("paused");
128+
reduced.matches = false;
129+
fine.matches = false;
130+
reduced.dispatchEvent(new Event("change"));
131+
movePointer(hero);
132+
expect(frames.size).toBe(0);
133+
expect(mark.properties.get("--home-motion-state")).toBe("running");
134+
});
135+
136+
it("pages every eight seconds, reverses at the end, and has no timer without overflow", () => {
137+
const { endorsements, observer } = fixture();
138+
expect(vi.getTimerCount()).toBe(0);
139+
observer.report(endorsements, true);
140+
vi.advanceTimersByTime(7_999);
141+
expect(endorsements.scrollTo).not.toHaveBeenCalled();
142+
vi.advanceTimersByTime(16_001);
143+
expect(endorsements.scrollTo.mock.calls.map(([options]) => options.left)).toEqual([
144+
400, 800, 400,
145+
]);
146+
expect(
147+
endorsements.scrollTo.mock.calls.every(([options]) => options.behavior === "smooth"),
148+
).toBe(true);
149+
150+
endorsements.clientWidth = endorsements.scrollWidth;
151+
viewport.dispatchEvent(new Event("resize"));
152+
expect(vi.getTimerCount()).toBe(0);
153+
expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" });
154+
endorsements.clientWidth = 400;
155+
viewport.dispatchEvent(new Event("resize"));
156+
expect(vi.getTimerCount()).toBe(1);
157+
});
158+
159+
it("pauses paging for hover, focus, hidden content, and reduced motion", () => {
160+
const { endorsements, observer } = fixture();
161+
observer.report(endorsements, true);
162+
const changeVisibility = (visible: boolean) => {
163+
page.visibilityState = visible ? "visible" : "hidden";
164+
page.dispatchEvent(new Event("visibilitychange"));
165+
};
166+
const changeMotion = (matches: boolean) => {
167+
reduced.matches = matches;
168+
reduced.dispatchEvent(new Event("change"));
169+
};
170+
const pauses = [
171+
[
172+
() => endorsements.dispatchEvent(new Event("pointerenter")),
173+
() => endorsements.dispatchEvent(new Event("pointerleave")),
174+
],
175+
[
176+
() => endorsements.dispatchEvent(new Event("focusin")),
177+
() =>
178+
endorsements.dispatchEvent(Object.assign(new Event("focusout"), { relatedTarget: null })),
179+
],
180+
[() => changeVisibility(false), () => changeVisibility(true)],
181+
[() => observer.report(endorsements, false), () => observer.report(endorsements, true)],
182+
[() => changeMotion(true), () => changeMotion(false)],
183+
] as const;
184+
for (const [pause, resume] of pauses) {
185+
pause();
186+
expect(vi.getTimerCount()).toBe(0);
187+
vi.advanceTimersByTime(16_000);
188+
resume();
189+
expect(vi.getTimerCount()).toBe(1);
190+
}
191+
expect(endorsements.scrollTo).not.toHaveBeenCalled();
192+
vi.advanceTimersByTime(8_000);
193+
expect(endorsements.scrollTo).toHaveBeenCalledWith({ left: 400, behavior: "smooth" });
194+
endorsements.dispatchEvent(new Event("pointerenter"));
195+
expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" });
196+
expect(vi.getTimerCount()).toBe(0);
197+
});
198+
199+
it.each(["wheel", "pointerdown", "keydown"])("hands control to the user after %s", (event) => {
200+
const { endorsements, observer } = fixture();
201+
observer.report(endorsements, true);
202+
endorsements.dispatchEvent(new Event(event));
203+
observer.report(endorsements, false);
204+
observer.report(endorsements, true);
205+
endorsements.dispatchEvent(new Event("pointerleave"));
206+
viewport.dispatchEvent(new Event("resize"));
207+
vi.advanceTimersByTime(60_000);
208+
expect(vi.getTimerCount()).toBe(0);
209+
expect(endorsements.scrollTo).not.toHaveBeenCalled();
210+
});
211+
212+
it("cancels pending work and ignores events after cleanup", () => {
213+
const { hero, mark, endorsements, observer } = fixture();
214+
observer.report(mark, true);
215+
observer.report(endorsements, true);
216+
movePointer(hero);
217+
dispose?.();
218+
observer.report(mark, true);
219+
movePointer(hero);
220+
reduced.dispatchEvent(new Event("change"));
221+
expect(observer.disconnect).toHaveBeenCalledTimes(1);
222+
expect(frames.size).toBe(0);
223+
expect(vi.getTimerCount()).toBe(0);
224+
expect(mark.properties.get("--home-motion-state")).toBe("paused");
225+
});
226+
});

0 commit comments

Comments
 (0)