perf(movimento): sem tela vazia, passagens em CSS e vitrine leve
Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
This commit is contained in:
1 parent
10f170abf4
commit
318a764190
77 files changed
+4293
-4951
No files matched your search
@@ -0,0 +1,141 @@
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { useRef } from "react";
|
||||
import { useInView } from "./useInView";
|
||||
|
||||
type Observer = {
|
||||
callback: IntersectionObserverCallback;
|
||||
options?: IntersectionObserverInit;
|
||||
observed: Element[];
|
||||
disconnect: jest.Mock;
|
||||
};
|
||||
let observers: Observer[] = [];
|
||||
|
||||
const original = global.IntersectionObserver;
|
||||
|
||||
beforeEach(() => {
|
||||
observers = [];
|
||||
Object.defineProperty(window, "innerHeight", {
|
||||
configurable: true,
|
||||
value: 800,
|
||||
});
|
||||
global.IntersectionObserver = class {
|
||||
private readonly record: Observer;
|
||||
constructor(
|
||||
callback: IntersectionObserverCallback,
|
||||
options?: IntersectionObserverInit,
|
||||
) {
|
||||
this.record = { callback, options, observed: [], disconnect: jest.fn() };
|
||||
observers.push(this.record);
|
||||
}
|
||||
observe(target: Element) {
|
||||
this.record.observed.push(target);
|
||||
}
|
||||
unobserve() {}
|
||||
disconnect() {
|
||||
this.record.disconnect();
|
||||
}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
} as unknown as typeof IntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.IntersectionObserver = original;
|
||||
});
|
||||
|
||||
function report(ratio: number, visibleHeight: number) {
|
||||
act(() => {
|
||||
for (const observer of observers)
|
||||
observer.callback(
|
||||
[
|
||||
{
|
||||
isIntersecting: ratio > 0,
|
||||
intersectionRatio: ratio,
|
||||
intersectionRect: { height: visibleHeight },
|
||||
target: observer.observed[0],
|
||||
} as unknown as IntersectionObserverEntry,
|
||||
],
|
||||
{} as IntersectionObserver,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function Probe({ amount, once }: { amount?: number; once?: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const inView = useInView(ref, { amount, once });
|
||||
return <div ref={ref} data-in-view={String(inView)} />;
|
||||
}
|
||||
|
||||
const state = (container: HTMLElement) =>
|
||||
container.firstElementChild?.getAttribute("data-in-view");
|
||||
|
||||
describe("useInView", () => {
|
||||
it("is false until enough of the element is on screen (20 % by default)", () => {
|
||||
const { container } = render(<Probe />);
|
||||
expect(state(container)).toBe("false");
|
||||
expect(observers).toHaveLength(1);
|
||||
expect(observers[0].observed).toHaveLength(1);
|
||||
|
||||
report(0.1, 40); // pouco à vista: ainda não
|
||||
expect(state(container)).toBe("false");
|
||||
|
||||
report(0.25, 100);
|
||||
expect(state(container)).toBe("true");
|
||||
});
|
||||
|
||||
it("stays true after the element leaves (once), and stops observing", () => {
|
||||
const { container } = render(<Probe />);
|
||||
report(1, 300);
|
||||
expect(state(container)).toBe("true");
|
||||
expect(observers[0].disconnect).toHaveBeenCalled();
|
||||
|
||||
report(0, 0);
|
||||
expect(state(container)).toBe("true");
|
||||
});
|
||||
|
||||
it("follows the element in and out when once is false", () => {
|
||||
const { container } = render(<Probe once={false} />);
|
||||
report(1, 300);
|
||||
expect(state(container)).toBe("true");
|
||||
|
||||
report(0, 0);
|
||||
expect(state(container)).toBe("false");
|
||||
|
||||
report(0.5, 200);
|
||||
expect(state(container)).toBe("true");
|
||||
});
|
||||
|
||||
it("counts a very tall element once a fifth of the window is showing, even if that is a tiny share of it", () => {
|
||||
const { container } = render(<Probe />);
|
||||
|
||||
report(0.02, 100); // 100 px de um bloco de 5000: menos de 20% da janela (800 px)
|
||||
expect(state(container)).toBe("false");
|
||||
|
||||
report(0.04, 200); // 200 px >= 20% de 800
|
||||
expect(state(container)).toBe("true");
|
||||
});
|
||||
|
||||
it("asks the observer for the amount it was given", () => {
|
||||
render(<Probe amount={0.7} />);
|
||||
|
||||
expect(observers[0].options?.threshold).toEqual([0, 0.7]);
|
||||
});
|
||||
|
||||
it("is true at once when the browser has no IntersectionObserver (content is never left hidden)", () => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
|
||||
(global as any).IntersectionObserver = undefined;
|
||||
|
||||
const { container } = render(<Probe />);
|
||||
|
||||
expect(state(container)).toBe("true");
|
||||
});
|
||||
|
||||
it("disconnects the observer when the component unmounts", () => {
|
||||
const { unmount } = render(<Probe />);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(observers[0].disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user