Warn user of unsaved outfit changes
This commit is contained in:
parent
a69bc8184a
commit
76aca48b43
4 changed files with 84 additions and 1 deletions
|
@ -92,4 +92,42 @@ describe("WardrobePage: Outfit saving", () => {
|
|||
cy.reload();
|
||||
page.getOutfitPreview({ timeout: 20000 }).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("prompts before navigating away from unsaved changes", () => {
|
||||
// Create stub methods to reject navigation confirmation prompts. We need
|
||||
// `confirm` for react-router's client-side nav, and `before:unload` for
|
||||
// the browser's built-in full-page nav prompts.
|
||||
const confirmStub = cy.stub().returns(false);
|
||||
cy.on("window:confirm", confirmStub);
|
||||
const beforeUnloadStub = cy.stub();
|
||||
cy.on("window:before:unload", beforeUnloadStub);
|
||||
|
||||
cy.logInAs("dti-test");
|
||||
cy.visit("/outfits/new?species=1&color=8");
|
||||
|
||||
// Give the outfit a unique timestamped name
|
||||
const outfitName = `Cypress Test - Block Navigation - ${new Date().toISOString()}`;
|
||||
page.getOutfitName({ timeout: 12000 }).click();
|
||||
cy.focused().type(outfitName + "{enter}");
|
||||
|
||||
// Save the outfit, but don't wait for it to finish.
|
||||
page.getSaveOutfitButton().click().should("have.attr", "data-loading");
|
||||
|
||||
// Click the big back button, and observe that it triggers a confirm prompt.
|
||||
// We'll reject it. HACK: It's a bit flaky if you try the clicks
|
||||
// immediately in sequence. Is there a better way to do this?
|
||||
page.showOutfitControls();
|
||||
cy.wait(100); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
page.getNavBackButton().click();
|
||||
cy.wrap(confirmStub).should("be.called");
|
||||
|
||||
// Try to reload the page, and observe that it would trigger the browser's
|
||||
// built-in prompt, i.e., `defaultPrevented`. Cypress will automatically
|
||||
// confirm the simulated prompt, and complete the reload.
|
||||
cy.reload();
|
||||
cy.wrap(beforeUnloadStub).should(
|
||||
"be.calledWith",
|
||||
Cypress.sinon.match((e) => e.defaultPrevented)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const withTestId = (testId) => (options) =>
|
||||
cy.get(`[data-test-id="${CSS.escape(testId)}"]`, options);
|
||||
|
||||
export const getNavBackButton = withTestId("wardrobe-nav-back-button");
|
||||
export const getSpeciesSelect = withTestId("wardrobe-species-picker");
|
||||
export const getColorSelect = withTestId("wardrobe-color-picker");
|
||||
export const getPosePickerButton = withTestId("wardrobe-pose-picker");
|
||||
|
@ -38,3 +39,7 @@ export function getOutfitPreview(options = { timeout: 15000 }) {
|
|||
.get("img")
|
||||
.first();
|
||||
}
|
||||
|
||||
export function showOutfitControls(options) {
|
||||
return cy.get("[data-test-id=wardrobe-outfit-controls]", options).click();
|
||||
}
|
||||
|
|
|
@ -146,6 +146,7 @@ function OutfitControls({
|
|||
setFocusIsLocked(true);
|
||||
}
|
||||
}}
|
||||
data-test-id="wardrobe-outfit-controls"
|
||||
>
|
||||
<Box gridArea="back" onClick={maybeUnlockFocus}>
|
||||
<BackButton outfitState={outfitState} />
|
||||
|
@ -297,6 +298,7 @@ function BackButton({ outfitState }) {
|
|||
icon={<ArrowBackIcon />}
|
||||
aria-label="Leave this outfit"
|
||||
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
|
||||
data-test-id="wardrobe-nav-back-button"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { loadable } from "../util";
|
||||
|
||||
|
@ -29,7 +30,8 @@ function WardrobePage() {
|
|||
|
||||
// We manage outfit saving up here, rather than at the point of the UI where
|
||||
// "Saving" indicators appear. That way, auto-saving still happens even when
|
||||
// the indicator isn't on the page, e.g. when searching.
|
||||
// the indicator isn't on the page, e.g. when searching. We also mount a
|
||||
// <Prompt /> in this component to prevent navigating away before saving.
|
||||
const outfitSaving = useOutfitSaving(outfitState, dispatchToOutfit);
|
||||
|
||||
usePageTitle(outfitState.name || "Untitled outfit");
|
||||
|
@ -49,6 +51,27 @@ function WardrobePage() {
|
|||
}
|
||||
}, [error, toast]);
|
||||
|
||||
// For new outfits, we only block navigation while saving. For existing
|
||||
// outfits, we block navigation while there are any unsaved changes.
|
||||
const shouldBlockNavigation =
|
||||
outfitSaving.canSaveOutfit &&
|
||||
((outfitSaving.isNewOutfit && outfitSaving.isSaving) ||
|
||||
(!outfitSaving.isNewOutfit && !outfitSaving.latestVersionIsSaved));
|
||||
|
||||
// In addition to a <Prompt /> for client-side nav, we need to block full nav!
|
||||
React.useEffect(() => {
|
||||
if (shouldBlockNavigation) {
|
||||
const onBeforeUnload = (e) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
||||
}
|
||||
}, [shouldBlockNavigation]);
|
||||
|
||||
// NOTE: Most components pass around outfitState directly, to make the data
|
||||
// relationships more explicit... but there are some deep components
|
||||
// that need it, where it's more useful and more performant to access
|
||||
|
@ -58,6 +81,21 @@ function WardrobePage() {
|
|||
<SupportOnly>
|
||||
<WardrobeDevHacks />
|
||||
</SupportOnly>
|
||||
|
||||
{/*
|
||||
* TODO: This might unnecessarily block navigations that we don't
|
||||
* necessarily need to, e.g., navigating back to Your Outfits while the
|
||||
* save request is in flight. We could instead submit the save mutation
|
||||
* immediately on client-side nav, and have each outfit save mutation
|
||||
* install a `beforeunload` handler that ensures that you don't close
|
||||
* the page altogether while it's in flight. But let's start simple and
|
||||
* see how annoying it actually is in practice lol
|
||||
*/}
|
||||
<Prompt
|
||||
when={shouldBlockNavigation}
|
||||
message="Are you sure you want to leave? Your changes might not be saved."
|
||||
/>
|
||||
|
||||
<WardrobePageLayout
|
||||
previewAndControls={
|
||||
<WardrobePreviewAndControls
|
||||
|
|
Loading…
Reference in a new issue