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
52 changes: 11 additions & 41 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@tiptap/pm": "^2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@zumer/snapdom": "^2.24.1",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"css-loader": "^6.5.1",
Expand All @@ -47,7 +48,6 @@
"file-loader": "^6.2.0",
"geotiff": "2.1.3",
"html-react-parser": "^5.1.18",
"html2canvas": "^1.4.1",
"json5": "^2.2.3",
"ol": "10.4.0",
"ol-mapbox-style": "12.4.0",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ authors = [{ name = "Corey Krewson", email = "ckrewson@aquaveo.com" }]
license-files = ["LICENSE"]
keywords = [""]
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"hjson==3.1",
"nh3==0.2.21",
Expand Down
57 changes: 50 additions & 7 deletions reactapp/__tests__/components/dashboard/DashboardItem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,7 @@ test("Dashboard Item fill viewport does not force a z-index (stacks by grid orde
);
});

test("Dashboard Item fill viewport shows grid size with indicator while editing", async () => {
test("Dashboard Item fill viewport fills the content area while editing too", async () => {
const mockedDashboard = JSON.parse(JSON.stringify(userDashboard));
const gridItem = mockedDashboard.tabs[0].gridItems[0];
gridItem.metadata_string = JSON.stringify({ fillViewport: true });
Expand Down Expand Up @@ -1784,12 +1784,17 @@ test("Dashboard Item fill viewport shows grid size with indicator while editing"

const dashboardGridItem = await screen.findByLabelText("gridItemDiv");
expect(await screen.findByTestId("editing")).toHaveTextContent("editing");
// In edit mode the item keeps grid sizing (not fixed) so it stays editable.
expect(
window.getComputedStyle(dashboardGridItem).getPropertyValue("position"),
).not.toBe("fixed");
// An indicator tells the creator the setting is active even though it is not
// rendered full-size while editing.
/* Filling applies while editing as well, so the creator sees the result as
soon as the cell is saved instead of having to leave edit mode. It also
keeps the item at its final size continuously: when filling was gated on
view mode, leaving edit mode resized the item and a map's canvas inside it
was still at grid size when the dashboard thumbnail was captured. */
await waitFor(() => {
expect(
window.getComputedStyle(dashboardGridItem).getPropertyValue("position"),
).toBe("fixed");
});
// The indicator still labels the setting while editing.
expect(
await screen.findByLabelText("fill-viewport-indicator"),
).toBeInTheDocument();
Expand Down Expand Up @@ -2913,3 +2918,41 @@ describe("validateGridItemBatch", () => {
expect(result.errors).toEqual([]);
});
});

// A fill item is position:fixed, and a DOM-to-image library has to reposition it
// to render it, which does not preserve where it sat among its siblings. Items
// meant to stay on top therefore say so with a z-index instead of relying on
// tree order, so a captured thumbnail matches the screen.
test("Dashboard Item context menu lives inside the item, not beside it", async () => {
const mockedDashboard = JSON.parse(JSON.stringify(userDashboard));
const gridItem = mockedDashboard.tabs[0].gridItems[0];

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, inEditing: true },
}),
);

const dashboardGridItem = await screen.findByLabelText("gridItemDiv");
const dropdownToggle = await screen.findByLabelText(
"dashboard-item-dropdown-toggle",
);
/* As a sibling it was positioned against the react-grid-layout wrapper, so it
stayed at the old grid position when a fill-viewport item moved to cover the
content area, and it did not follow a cell lifted above a fill item. */
expect(dashboardGridItem).toContainElement(dropdownToggle);
});
110 changes: 110 additions & 0 deletions reactapp/__tests__/components/dashboard/DashboardLayout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,113 @@ test("Dashboard Responsive Layout with allowOverlap", async () => {
layoutContextBefore,
);
});

describe("fill-viewport stacking", () => {
const makeItem = (i, metadata) => ({
id: Number(i),
uuid: `some-uuid-${i}`,
i,
x: 0,
y: 0,
w: 20,
h: 20,
source: "",
args_string: "{}",
metadata_string: JSON.stringify(metadata),
});

// Index 1 fills; index 0 sits before it, index 2 after it.
const gridItems = [
makeItem("1", {}),
makeItem("2", { fillViewport: true }),
makeItem("3", {}),
];

const renderLayout = (items, tabId = userDashboard.tabs[0].id) => {
const dashboard = JSON.parse(JSON.stringify(userDashboard));
dashboard.tabs[0].gridItems = items;
return render(
createLoadedComponent({
children: (
<LayoutAlertContextProvider>
<DashboardLayout tabId={tabId} gridItems={items} />
</LayoutAlertContextProvider>
),
options: { initialDashboard: dashboard, inEditing: true },
}),
);
};

/* Reading the grid item wrappers directly is the point of these tests: the
z-index sits on react-grid-layout's own element, which has no accessible
role or label to query by. */
const gridItemStyles = (container) =>
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
[...container.querySelectorAll(".react-grid-item")].map(
(element) => element.style.zIndex,
);

/* The lift has to sit on the grid item rather than anything inside it:
react-grid-layout renders its resize handles as siblings of the item's
content, so lifting only the content would paint over the handles and make
the tile impossible to resize. */
it("lifts only the items ordered after the fill item", async () => {
const { container } = renderLayout(gridItems);
expect(await screen.findAllByText("Rendered Item")).not.toHaveLength(0);
expect(gridItemStyles(container)).toEqual(["", "", "1"]);
});

it("lifts nothing when no item fills", async () => {
const { container } = renderLayout([makeItem("1", {}), makeItem("2", {})]);
expect(await screen.findAllByText("Rendered Item")).not.toHaveLength(0);
expect(gridItemStyles(container)).toEqual(["", ""]);
});

// Fill-viewport does not apply on the popup surface, so nothing is lifted.
it("lifts nothing on the popup surface", async () => {
const { container } = renderLayout(gridItems, "popup");
expect(await screen.findAllByText("Rendered Item")).not.toHaveLength(0);
expect(gridItemStyles(container)).toEqual(["", "", ""]);
});

/* The filling item sizes itself from the viewport, not the grid, so a resize
handle on it does nothing - and being a sibling of the item's content, it
would be stranded at the old grid position once the item goes
position:fixed. Marking it non-resizable is what removes the handle:
react-grid-layout keeps the element in the DOM and hides it by adding
react-resizable-hide, whose rule lives in its own stylesheet
(.react-resizable-hide > .react-resizable-handle { display: none }). */
const gridItemFlags = (container) =>
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
[...container.querySelectorAll(".react-grid-item")].map((element) => ({
handleHidden: element.classList.contains("react-resizable-hide"),
draggable: element.classList.contains("react-draggable"),
}));

it("hides the fill item's resize handle, while the others keep theirs", async () => {
const { container } = renderLayout(gridItems);
expect(await screen.findAllByText("Rendered Item")).not.toHaveLength(0);
// Index 1 fills; the tiles either side of it stay resizable and draggable.
expect(gridItemFlags(container)).toEqual([
{ handleHidden: false, draggable: true },
{ handleHidden: true, draggable: false },
{ handleHidden: false, draggable: true },
]);
});

/* The flags come off the item's metadata every render, so clearing the
setting has to restore dragging and the handle with no further action. */
it("restores the handle and dragging once nothing fills", async () => {
const withoutFill = gridItems.map((item) => ({
...item,
metadata_string: JSON.stringify({}),
}));
const { container } = renderLayout(withoutFill);
expect(await screen.findAllByText("Rendered Item")).not.toHaveLength(0);
expect(gridItemFlags(container)).toEqual([
{ handleHidden: false, draggable: true },
{ handleHidden: false, draggable: true },
{ handleHidden: false, draggable: true },
]);
});
});
28 changes: 28 additions & 0 deletions reactapp/__tests__/components/landingPage/DashboardCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,9 @@ test("DashboardCard editable, edit thumbnail", async () => {
expect(mockUpdateDashboard).toHaveBeenCalledWith(
{
id: userDashboard.id,
// Uploading by hand turns off auto-capture, so the next save cannot
// overwrite the image just chosen.
autoThumbnail: false,
image: "data:image/png;base64,testImage",
},
"SxICmOkFldX4o4YVaySdZq9sgn0eRd3Ih6uFtY8BgU5tMyZc7n90oJ4M2My5i7cy",
Expand Down Expand Up @@ -937,6 +940,9 @@ test("DashboardCard editable, edit thumbnail fail", async () => {
expect(mockUpdateDashboard).toHaveBeenCalledWith(
{
id: userDashboard.id,
// Uploading by hand turns off auto-capture, so the next save cannot
// overwrite the image just chosen.
autoThumbnail: false,
image: "data:image/png;base64,testImage",
},
"SxICmOkFldX4o4YVaySdZq9sgn0eRd3Ih6uFtY8BgU5tMyZc7n90oJ4M2My5i7cy",
Expand Down Expand Up @@ -1654,3 +1660,25 @@ TestingComponent.propTypes = {
PropTypes.element,
]),
};

test("DashboardCard renders no image until one exists", async () => {
const imagelessDashboard = JSON.parse(JSON.stringify(userDashboard));
imagelessDashboard.image = null;

render(
createLoadedComponent({
children: (
<MemoryRouter initialEntries={["/"]}>
<DashboardCard {...imagelessDashboard} />
</MemoryRouter>
),
}),
);

expect(await screen.findByText(imagelessDashboard.name)).toBeInTheDocument();
/* Rendering the element with no src would show a broken-image icon, which
reads worse than an empty card. */
expect(
screen.queryByLabelText("Dashboard Card Image"),
).not.toBeInTheDocument();
});
Loading
Loading