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({}, "", "/");
});
});