Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 0 additions & 92 deletions reactapp/__tests__/components/dashboard/DashboardItem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
within,
fireEvent,
waitFor,
cleanup,
} from "@testing-library/react";
import DashboardItem, {
handleGridItemExport,
Expand Down Expand Up @@ -1656,97 +1655,6 @@ test("Dashboard attribution and not show", async () => {
).not.toBeInTheDocument();
});

// jsdom's computed style does not resolve :has(), so the raise cannot be read
// back through getComputedStyle. Inspecting the injected rule is the next best
// thing: it pins that the rule ships, what it raises to, and that it is scoped to
// the fill-viewport branch rather than applied to every grid item.
const injectedCss = () =>
Array.from(document.styleSheets)
.flatMap((sheet) => {
try {
return Array.from(sheet.cssRules).map((rule) => rule.cssText);
} catch {
return [];
}
})
.join("\n");

const raiseRule = () =>
injectedCss()
.split("\n")
.find((rule) => rule.includes('data-map-control-open="true"'));

// The class styled-components generated for the fill-viewport block. Asserting
// class membership on the element is order-independent, unlike asserting the rule
// is absent from the stylesheet -- styled-components keeps injected rules for the
// whole test file, so a rule from an earlier test is still present.
const raiseRuleClass = () => {
const rule = raiseRule();
const match = rule && rule.match(/^\.([\w-]+)/);
return match ? match[1] : null;
};

const renderGridItem = ({ fillViewport }) => {
const mockedDashboard = JSON.parse(JSON.stringify(userDashboard));
const gridItem = mockedDashboard.tabs[0].gridItems[0];
gridItem.metadata_string = JSON.stringify(
fillViewport ? { fillViewport: true } : {},
);

return render(
createLoadedComponent({
children: (
<GridItemContext.Provider
value={{
gridItemSource: gridItem.source,
gridItemI: gridItem.i,
gridItemMetadataString: gridItem.metadata_string,
gridItemArgsString: gridItem.args_string,
gridItemIndex: 0,
enableFillViewport: true,
}}
>
<DashboardItem />
</GridItemContext.Provider>
),
options: { initialDashboard: mockedDashboard },
}),
);
};

test("Dashboard Item fill viewport raises the tile for an open map control", async () => {
// position:fixed seals the item into its own stacking context, so a map's
// legend or layer control cannot paint above a later grid item on its own.
renderGridItem({ fillViewport: true });
const item = await screen.findByLabelText("gridItemDiv");

const rule = raiseRule();
expect(rule).toBeDefined();
expect(rule).toMatch(/z-index:\s*1029/);
// Below the fixed header and every modal layer, so a modal still covers the map.
expect(rule).not.toMatch(/z-index:\s*10[4-9]\d/);
// And the rule actually applies to this item.
expect(item.classList.contains(raiseRuleClass())).toBe(true);
});

test("Dashboard Item without fill viewport is not covered by the raise rule", async () => {
// A non-fill item is position:relative / z-index:auto, so it is not a stacking
// context and the control's own z-index already escapes. Scoping the rule to
// the fill branch keeps that path untouched.
renderGridItem({ fillViewport: true });
await screen.findByLabelText("gridItemDiv");
const fillClass = raiseRuleClass();
expect(fillClass).not.toBeNull();
cleanup();

renderGridItem({ fillViewport: false });
const item = await screen.findByLabelText("gridItemDiv");
expect(window.getComputedStyle(item).getPropertyValue("position")).toBe(
"relative",
);
expect(item.classList.contains(fillClass)).toBe(false);
});

test("Dashboard Item fill viewport fills the content area in view mode", async () => {
const mockedDashboard = JSON.parse(JSON.stringify(userDashboard));
const gridItem = mockedDashboard.tabs[0].gridItems[0];
Expand Down
210 changes: 210 additions & 0 deletions reactapp/__tests__/components/map/FloatingMapControl.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { render, screen, act } from "@testing-library/react";
import FloatingMapControl, {
FLOATING_CONTROL_Z_INDEX,
styleFromAnchor,
} from "components/map/FloatingMapControl";

// jsdom does no layout, so every rect is stubbed. These tests pin the mapping
// from anchor rect to fixed-position style and the escape from the parent tree;
// they cannot prove paint order.
const VIEWPORT = { width: 1000, height: 800 };

const stubRect = (rect) =>
jest
.spyOn(Element.prototype, "getBoundingClientRect")
.mockReturnValue({ ...rect, toJSON: () => ({}) });

beforeEach(() => {
window.innerWidth = VIEWPORT.width;
window.innerHeight = VIEWPORT.height;
});

afterEach(() => {
jest.restoreAllMocks();
});

describe("styleFromAnchor", () => {
// A bottom-left anchor collapses to a point once its content is portalled
// away, so only left/bottom carry meaning -- right/top are the same point and
// say nothing about the control's size.
test("bottom-left pins the corner and leaves size to the content", () => {
const style = styleFromAnchor(
{ left: 16, right: 16, top: 700, bottom: 700, width: 0, height: 0 },
["bottom", "left"],
VIEWPORT,
);
expect(style).toEqual({ left: "16px", bottom: "100px" });
});

test("bottom-right measures from the far edges", () => {
const style = styleFromAnchor(
{ left: 984, right: 984, top: 700, bottom: 700, width: 0, height: 0 },
["bottom", "right"],
VIEWPORT,
);
expect(style).toEqual({ right: "16px", bottom: "100px" });
});

test("pinned on both sides carries the width across", () => {
// The alert spans the map, so the floated copy must not shrink to content.
const style = styleFromAnchor(
{ left: 16, right: 984, top: 16, bottom: 16, width: 968, height: 0 },
["top", "left", "right"],
VIEWPORT,
);
expect(style).toEqual({ left: "16px", top: "16px", width: "968px" });
expect(style.right).toBeUndefined();
});

test("no rect yields no style", () => {
expect(styleFromAnchor(null, ["bottom", "left"], VIEWPORT)).toBeNull();
});
});

describe("FloatingMapControl", () => {
test("renders its children outside the parent tree", () => {
stubRect({
left: 16,
right: 16,
top: 700,
bottom: 700,
width: 0,
height: 0,
});
render(
<div data-testid="map-tile">
<FloatingMapControl edges={["bottom", "left"]}>
<button type="button">Show Legend</button>
</FloatingMapControl>
</div>,
);

const control = screen.getByRole("button", { name: "Show Legend" });
expect(control).toBeInTheDocument();
// The whole point: it must not be a descendant of the tile, or it stays
// sealed inside that tile's stacking context.
expect(screen.getByTestId("map-tile")).not.toContainElement(control);
expect(document.body).toContainElement(control);
});

test("positions the floated copy from the anchor's rect", () => {
stubRect({
left: 16,
right: 16,
top: 700,
bottom: 700,
width: 0,
height: 0,
});
render(
<FloatingMapControl edges={["bottom", "left"]}>
<span>content</span>
</FloatingMapControl>,
);

const floated = screen.getByTestId("floating-map-control");
expect(floated).toHaveStyle({
position: "fixed",
left: "16px",
bottom: "100px",
});
expect(floated).toHaveStyle({ zIndex: String(FLOATING_CONTROL_Z_INDEX) });
});

test("repositions when the window resizes", () => {
const rect = stubRect({
left: 16,
right: 16,
top: 700,
bottom: 700,
width: 0,
height: 0,
});
render(
<FloatingMapControl edges={["bottom", "left"]}>
<span>content</span>
</FloatingMapControl>,
);
expect(screen.getByTestId("floating-map-control")).toHaveStyle({
bottom: "100px",
});

// The map got shorter: same anchor offset from the bottom, different
// viewport, so the computed `bottom` has to change.
rect.mockReturnValue({
left: 16,
right: 16,
top: 500,
bottom: 500,
width: 0,
height: 0,
toJSON: () => ({}),
});
window.innerHeight = 600;
act(() => {
window.dispatchEvent(new Event("resize"));
});

expect(screen.getByTestId("floating-map-control")).toHaveStyle({
bottom: "100px",
left: "16px",
});
});

test("removes its listeners and observer on unmount", () => {
stubRect({
left: 16,
right: 16,
top: 700,
bottom: 700,
width: 0,
height: 0,
});
const addSpy = jest.spyOn(window, "addEventListener");
const removeSpy = jest.spyOn(window, "removeEventListener");
const disconnect = jest.fn();
const observe = jest.fn();
const original = global.ResizeObserver;
global.ResizeObserver = jest.fn(() => ({ observe, disconnect }));

const { unmount } = render(
<FloatingMapControl edges={["bottom", "left"]}>
<span>content</span>
</FloatingMapControl>,
);
expect(addSpy).toHaveBeenCalledWith("resize", expect.any(Function));
expect(addSpy).toHaveBeenCalledWith("scroll", expect.any(Function), true);

unmount();
expect(removeSpy).toHaveBeenCalledWith("resize", expect.any(Function));
expect(removeSpy).toHaveBeenCalledWith(
"scroll",
expect.any(Function),
true,
);
global.ResizeObserver = original;
});

test("the anchor stays behind and is inert", () => {
stubRect({
left: 16,
right: 16,
top: 700,
bottom: 700,
width: 0,
height: 0,
});
render(
<FloatingMapControl edges={["bottom", "left"]} className="anchor-class">
<span>content</span>
</FloatingMapControl>,
);

// The caller's positioning CSS rides on the anchor, so it has to remain in
// place rather than move to the portal.
const anchor = screen.getByTestId("floating-map-control-anchor");
expect(anchor).toHaveClass("anchor-class");
expect(anchor).toHaveAttribute("aria-hidden", "true");
expect(anchor).toBeEmptyDOMElement();
});
});
21 changes: 0 additions & 21 deletions reactapp/__tests__/components/map/LayersControl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,24 +251,3 @@ describe("parseProgress", () => {
expect(parseProgress(message)).toBeNull();
});
});

test("LayersControl flags itself only while expanded", async () => {
render(
<LayersControl updater={null} visualizationRef={{ current: undefined }} />,
);

expect(screen.getByLabelText("Layers Control")).not.toHaveAttribute(
"data-map-control-open",
);

fireEvent.click(await screen.findByLabelText("Show Layers Control"));
expect(screen.getByLabelText("Layers Control")).toHaveAttribute(
"data-map-control-open",
"true",
);

fireEvent.click(await screen.findByLabelText("Close Layers Control"));
expect(screen.getByLabelText("Layers Control")).not.toHaveAttribute(
"data-map-control-open",
);
});
18 changes: 0 additions & 18 deletions reactapp/__tests__/components/map/Legend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,3 @@ test("LegendControl", async () => {
fireEvent.click(closeLegendButton);
expect(screen.queryByText("Some New Title")).not.toBeInTheDocument();
});

test("LegendControl flags itself only while expanded", async () => {
// The flag is what DashboardItem's fill-viewport rule keys off to raise the
// whole tile. Raising the tile is the only lever available: position:fixed
// makes it a stacking context, so no z-index on the control can escape it.
render(<LegendControl legendItems={[legendItems]} />);

fireEvent.click(await screen.findByLabelText("Show Legend Control"));
expect(await screen.findByLabelText("Legend Control")).toHaveAttribute(
"data-map-control-open",
"true",
);

fireEvent.click(await screen.findByLabelText("Close Legend Control"));
expect(screen.getByLabelText("Legend Control")).not.toHaveAttribute(
"data-map-control-open",
);
});
Loading
Loading