diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index eb96e2d26..df4aa7cc4 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -160,6 +160,7 @@ const WebComponentLoader = (props) => { remixLoadFailed, initialProject, locale, + embedded, }); useProjectPersistence({ diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js index e0358e707..2278579e9 100644 --- a/src/containers/WebComponentLoader.test.js +++ b/src/containers/WebComponentLoader.test.js @@ -262,9 +262,28 @@ describe("When no user is in state", () => { loadCache: true, remixLoadFailed: false, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); + test("Passes embedded prop to useProject hook", () => { + render( + + + + + , + ); + + expect(useProject).toHaveBeenLastCalledWith( + expect.objectContaining({ embedded: true }), + ); + }); + test("Calls useProjectPersistence hook with correct attributes", () => { expect(useProjectPersistence).toHaveBeenCalledWith({ project: { @@ -409,6 +428,7 @@ describe("When no user is in state", () => { loadCache: true, remixLoadFailed: false, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); }); @@ -536,6 +556,7 @@ describe("When user is in state", () => { loadCache: true, remixLoadFailed: false, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); @@ -568,6 +589,7 @@ describe("When user is in state", () => { loadCache: true, remixLoadFailed: false, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); }); @@ -650,6 +672,7 @@ describe("When user is in state", () => { loadCache: true, remixLoadFailed: true, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); @@ -813,6 +836,7 @@ describe("when a Scratch remix updates the project identifier", () => { loadCache: true, remixLoadFailed: false, reactAppApiEndpoint: "http://localhost:3009", + embedded: false, }); }); }); diff --git a/src/hooks/useProject.js b/src/hooks/useProject.js index dc0a71455..bb0de018e 100644 --- a/src/hooks/useProject.js +++ b/src/hooks/useProject.js @@ -16,17 +16,23 @@ export const useProject = ({ loadCache = true, remixLoadFailed = false, locale = null, + embedded = false, }) => { const loading = useSelector((state) => state.editor.loading); const isEmbedded = useSelector((state) => state.editor.isEmbedded); const isBrowserPreview = useSelector((state) => state.editor.browserPreview); + const browserPreviewFromQuery = + new URLSearchParams(window.location.search).get("browserPreview") === + "true"; + const isEmbeddedMode = embedded || isEmbedded; + const canUseBrowserPreviewCache = + isBrowserPreview || (embedded && browserPreviewFromQuery); + const shouldSkipCache = isEmbeddedMode && !canUseBrowserPreviewCache; const project = useSelector((state) => state.editor.project); const loadDispatched = useRef(false); const getCachedProject = (id) => - isEmbedded && !isBrowserPreview - ? null - : JSON.parse(localStorage.getItem(id || "project")); + shouldSkipCache ? null : JSON.parse(localStorage.getItem(id || "project")); const [cachedProject, setCachedProject] = useState( getCachedProject(projectIdentifier), ); @@ -51,21 +57,23 @@ export const useProject = ({ return; } - const is_cached_saved_project = + const isCachedSavedProject = projectIdentifier && cachedProject && cachedProject.identifier === projectIdentifier; - const is_cached_unsaved_project = + const isCachedUnsavedProject = !projectIdentifier && cachedProject && !initialProject; + const cachedProjectMatchesRequest = + isCachedSavedProject || isCachedUnsavedProject; - // At the moment this will never match because the cachedProject doesn't have a locale attribute (yet), - // so this will always be false, which effectively disables the whole caching mechanism + // Browser previews need the current local edits. Starter projects can be + // served from a fallback locale, so the cached locale may not match the URL. const cachedLocaleMatches = cachedProject?.locale === effectiveLocale; if ( loadCache && - (is_cached_saved_project || is_cached_unsaved_project) && - cachedLocaleMatches + cachedProjectMatchesRequest && + (cachedLocaleMatches || canUseBrowserPreviewCache) ) { loadCachedProject(); return; diff --git a/src/hooks/useProject.test.js b/src/hooks/useProject.test.js index 488022f01..6c1ed9893 100644 --- a/src/hooks/useProject.test.js +++ b/src/hooks/useProject.test.js @@ -76,7 +76,7 @@ describe("When not embedded", () => { expect(setProject).toHaveBeenCalledWith(initialProject); }); - test("If cached project matches identifer and locale, uses cached project", () => { + test("If cached project matches identifier and locale, uses cached project", () => { localStorage.setItem( cachedProject.identifier, JSON.stringify(cachedProject), @@ -92,7 +92,7 @@ describe("When not embedded", () => { expect(setProject).toHaveBeenCalledWith(cachedProject); }); - test("If cached project matches identifer and locale, clears cached project", () => { + test("If cached project matches identifier and locale, keeps cached project", () => { localStorage.setItem( cachedProject.identifier, JSON.stringify(cachedProject), @@ -105,10 +105,33 @@ describe("When not embedded", () => { }), { wrapper }, ); - expect(localStorage.getItem("project")).toBeNull(); + expect(JSON.parse(localStorage.getItem(cachedProject.identifier))).toEqual( + cachedProject, + ); }); - test("If cached project does not match identifer, does not use cached project", async () => { + test("If embedded prop is true before embedded state is set, loads from server instead of cache", () => { + syncProject.mockImplementation(jest.fn((_) => jest.fn())); + localStorage.setItem( + cachedProject.identifier, + JSON.stringify(cachedProject), + ); + renderHook( + () => + useProject({ + projectIdentifier: cachedProject.identifier, + locale: cachedProject.locale, + embedded: true, + accessToken, + reactAppApiEndpoint, + }), + { wrapper }, + ); + expect(syncProject).toHaveBeenCalledWith("load"); + expect(setProject).not.toHaveBeenCalledWith(cachedProject); + }); + + test("If cached project does not match identifier, does not use cached project", async () => { syncProject.mockImplementationOnce(jest.fn((_) => jest.fn())); localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook( @@ -140,6 +163,31 @@ describe("When not embedded", () => { ); }); + test("If cached project does not match locale and browserPreview query is used outside embedded viewer, does not use cached project", () => { + syncProject.mockImplementation(jest.fn((_) => jest.fn())); + window.history.pushState( + {}, + "", + "/en-US/projects/hello-world-project?browserPreview=true&page=index.html", + ); + localStorage.setItem( + cachedProject.identifier, + JSON.stringify(cachedProject), + ); + renderHook( + () => + useProject({ + projectIdentifier: cachedProject.identifier, + locale: "en-US", + accessToken, + reactAppApiEndpoint, + }), + { wrapper }, + ); + expect(syncProject).toHaveBeenCalledWith("load"); + expect(setProject).not.toHaveBeenCalledWith(cachedProject); + }); + test("If cached project does not match identifier and locale, loads correct uncached project", async () => { syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); localStorage.setItem("project", JSON.stringify(cachedProject)); @@ -559,6 +607,7 @@ describe("When not embedded", () => { afterEach(() => { localStorage.clear(); + window.history.pushState({}, "", "/"); }); }); @@ -575,6 +624,35 @@ describe("When embedded", () => { wrapper = ({ children }) => {children}; }); + test("If embedded browser preview and cached project locale does not match, uses cached project", () => { + const browserPreviewCachedProject = { + ...cachedProject, + identifier: "blank-html-starter", + locale: "en", + }; + window.history.pushState( + {}, + "", + "/en-US/embed/viewer/blank-html-starter?browserPreview=true&page=index.html", + ); + localStorage.setItem( + browserPreviewCachedProject.identifier, + JSON.stringify(browserPreviewCachedProject), + ); + renderHook( + () => + useProject({ + projectIdentifier: browserPreviewCachedProject.identifier, + locale: "en-US", + embedded: true, + accessToken, + reactAppApiEndpoint, + }), + { wrapper }, + ); + expect(setProject).toHaveBeenCalledWith(browserPreviewCachedProject); + }); + test("If embedded and cached project, loads from server", async () => { syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); localStorage.setItem("hello-world-project", JSON.stringify(cachedProject)); @@ -601,5 +679,6 @@ describe("When embedded", () => { afterEach(() => { localStorage.clear(); + window.history.pushState({}, "", "/"); }); });