diff --git a/source/App.js b/source/App.js
index 857c811f..f1379dd1 100644
--- a/source/App.js
+++ b/source/App.js
@@ -35,8 +35,7 @@ import "./css/App.scss";
import { signInAnonymously } from "firebase/auth";
import { getSoundProfileStatement } from "./components/firebase_soundProfile";
import { captureError } from "./sentry";
-import { fetchGlossaryData } from "./components/glossaryApi";
-import { initGlossary } from "../threshold/parameters/glossaryRegistry";
+import { getGlossaryFull } from "../threshold/parameters/glossaryRegistry";
// Utility function to create empty resources object from constants
const createEmptyResourcesObject = () => {
@@ -102,7 +101,6 @@ export default class App extends Component {
profileStatement: "Loading ...",
isCompiledFromArchiveBool: false,
archivedZip: null,
- glossaryData: null,
compileWarnings: [],
};
@@ -145,13 +143,6 @@ export default class App extends Component {
}
async componentDidMount() {
- fetchGlossaryData()
- .then((data) => {
- initGlossary(data);
- this.setState({ glossaryData: data });
- })
- .catch((error) => console.warn("Failed to fetch glossary data:", error));
-
// get the actual changes from GitHub
try {
const websiteGitHubRepo = await fetch(
@@ -707,12 +698,9 @@ export default class App extends Component {
isCompiledFromArchiveBool,
archivedZip,
resourcesLoaded,
- glossaryData,
compileWarnings,
} = this.state;
- if (glossaryData === null) return null;
-
const steps = [];
const viewingPreviousExperiment =
@@ -742,7 +730,6 @@ export default class App extends Component {
isCompiledFromArchiveBool={isCompiledFromArchiveBool}
archivedZip={archivedZip}
resourcesLoaded={resourcesLoaded}
- glossaryData={glossaryData}
/>,
);
else
@@ -765,7 +752,6 @@ export default class App extends Component {
isCompiledFromArchiveBool={isCompiledFromArchiveBool}
archivedZip={archivedZip}
resourcesLoaded={resourcesLoaded}
- glossaryData={glossaryData}
compileWarnings={compileWarnings}
/>,
);
@@ -776,7 +762,7 @@ export default class App extends Component {
>}>
)}
diff --git a/source/Running.js b/source/Running.js
index 3d0084e5..f5b5ef60 100644
--- a/source/Running.js
+++ b/source/Running.js
@@ -637,7 +637,6 @@ export default class Running extends Component {
incompatibleCompletionCode,
abortedCompletionCode,
prolificToken,
- this.props.glossaryData,
);
if (result?.status === "UNPUBLISHED" && result.id) {
diff --git a/source/Table.js b/source/Table.js
index fe024f72..dc3aa200 100644
--- a/source/Table.js
+++ b/source/Table.js
@@ -69,36 +69,46 @@ export default class Table extends Component {
}
async handleTable(file) {
- const { user } = this.props;
-
+ // The glossary is fetched lazily on first compile (no longer at app launch).
+ // handleDrop has already opened a "Compiling ..." dialog before calling us;
+ // we relabel that same dialog for each phase instead of firing/closing our
+ // own, so the modal stays open continuously — closing it would leave a blank
+ // screen through the rest of the compile.
+ let shouldFetch = true;
+ let serverVersion = null;
try {
- let shouldFetch = true;
- let serverVersion = null;
- try {
- ({ version: serverVersion } = await fetchGlossaryVersion());
- const cachedVersion = getGlossaryVersion();
- if (
- serverVersion !== null &&
- cachedVersion !== null &&
- serverVersion === cachedVersion
- ) {
- shouldFetch = false;
- }
- } catch {
- // fall through to full fetch
+ ({ version: serverVersion } = await fetchGlossaryVersion());
+ const cachedVersion = getGlossaryVersion();
+ if (
+ serverVersion !== null &&
+ cachedVersion !== null &&
+ serverVersion === cachedVersion
+ ) {
+ shouldFetch = false;
}
+ } catch {
+ // fall through to full fetch
+ }
- if (shouldFetch) {
+ if (shouldFetch) {
+ // The glossary isn't ready yet; tell the scientist we're waiting on it.
+ manuallySetSwalTitle("Loading glossary …");
+ Swal.showLoading(null);
+ try {
// Fetch by explicit version so the CDN returns the just-published
// glossary (new version = new URL = cache miss), never a stale copy.
// If the probe failed, serverVersion is null → falls back to current.
const data = await fetchGlossaryData(serverVersion);
initGlossary(data);
+ } catch (err) {
+ Swal.close();
+ console.error("Failed to refresh glossary:", err);
+ return;
}
- } catch (err) {
- console.error("Failed to refresh glossary:", err);
- return;
}
+ // Restore the compiling status before handing off to the resource/compile
+ // flow, which manages its own status dialog.
+ manuallySetSwalTitle("Compiling ...");
let resolvedResources;
diff --git a/source/__tests__/App.test.js b/source/__tests__/App.test.js
deleted file mode 100644
index 59f824f8..00000000
--- a/source/__tests__/App.test.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import React from "react";
-import { render, waitFor } from "@testing-library/react";
-import App from "../App";
-
-jest.mock("firebase/database", () => ({
- set: jest.fn(),
- ref: jest.fn(),
- get: jest.fn().mockResolvedValue({ val: () => ({ count: 0 }) }),
-}));
-
-jest.mock("@firebase/util", () => ({
- uuidv4: jest.fn(() => "test-uuid"),
-}));
-
-jest.mock("sweetalert2", () => ({
- fire: jest.fn(),
- showLoading: jest.fn(),
- close: jest.fn(),
-}));
-
-jest.mock("../Step", () => () => null);
-jest.mock("../Glossary", () => ({ default: () => null }));
-jest.mock("../StatusLines", () => () => null);
-
-jest.mock("../components/steps", () => ({
- allSteps: jest.fn(() => ["login", "table", "upload", "running"]),
-}));
-
-jest.mock("../../threshold/preprocess/gitlabUtils", () => ({
- getCompatibilityRequirementsForProject: jest.fn(),
- getExperimentStatus: jest.fn(),
- getOriginalFileNameForProject: jest.fn(),
- getRecruitmentServiceConfig: jest.fn(),
- getDurationForProject: jest.fn(),
- getProlificStudyId: jest.fn(),
- User: jest.fn(() => ({})),
- getCommonResourcesNames: jest.fn(),
-}));
-
-jest.mock("../../threshold/preprocess/retry", () => ({
- getRetryDelayMs: jest.fn(() => 0),
-}));
-
-jest.mock("../../threshold/preprocess/constants", () => ({
- resourcesFileTypes: ["fonts", "images"],
-}));
-
-jest.mock("../components/firebase", () => ({
- auth: {},
- db: {},
-}));
-
-jest.mock("../components/prolificIntegration", () => ({
- getProlificAccount: jest.fn(),
- getProlificStudySubmissions: jest.fn(),
-}));
-
-jest.mock("../../threshold/components/compatibilityCheck", () => ({
- getCompatibilityRequirements: jest.fn(),
-}));
-
-jest.mock("../../threshold/preprocess/global", () => ({
- compatibilityRequirements: { t: [] },
-}));
-
-jest.mock("firebase/auth", () => ({
- signInAnonymously: jest.fn().mockResolvedValue({}),
-}));
-
-jest.mock("../components/firebase_soundProfile", () => ({
- getSoundProfileStatement: jest.fn(),
-}));
-
-jest.mock("../sentry", () => ({
- captureError: jest.fn(),
-}));
-
-jest.mock("../components/glossaryApi", () => ({
- fetchGlossaryData: jest.fn(),
-}));
-
-jest.mock("../../threshold/parameters/glossaryRegistry", () => ({
- initGlossary: jest.fn(),
-}));
-
-global.fetch = jest.fn().mockResolvedValue({ ok: false });
-
-const mockGlossaryData = {
- version: "1.0",
- glossary: { param1: { name: "param1" } },
- glossaryFull: [],
- superMatchingParams: ["param1"],
-};
-
-describe("App", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- const { fetchGlossaryData } = require("../components/glossaryApi");
- fetchGlossaryData.mockResolvedValue(mockGlossaryData);
- global.fetch.mockResolvedValue({ ok: false });
- });
-
- it("calls initGlossary with fetched glossary data when fetchGlossaryData resolves", async () => {
- const { initGlossary } = require("../../threshold/parameters/glossaryRegistry");
-
- render();
-
- await waitFor(() => {
- expect(initGlossary).toHaveBeenCalledWith(mockGlossaryData);
- });
- });
-});
diff --git a/source/__tests__/Table.test.js b/source/__tests__/Table.test.js
index dccdd6ac..7e711d5d 100644
--- a/source/__tests__/Table.test.js
+++ b/source/__tests__/Table.test.js
@@ -1,6 +1,8 @@
import React from "react";
import { render } from "@testing-library/react";
import Table from "../Table";
+import Swal from "sweetalert2";
+import { preprocessExperimentFile } from "../../threshold/preprocess/main";
jest.mock("sweetalert2", () => ({
fire: jest.fn(),
@@ -221,3 +223,78 @@ describe("Table.handleTable", () => {
expect(initGlossary).toHaveBeenCalledWith(mockGlossaryData);
});
});
+
+describe("Table.handleTable glossary loading dialog", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const swalTitles = () => {
+ const { manuallySetSwalTitle } = require("../../threshold/preprocess/gitlabUtils");
+ return manuallySetSwalTitle.mock.calls.map(([title]) => title);
+ };
+
+ it("relabels the open dialog to 'Glossary …' while downloading, then restores 'Compiling ...' without closing it", async () => {
+ const { fetchGlossaryData, fetchGlossaryVersion } = require("../components/glossaryApi");
+ const { getGlossaryVersion } = require("../../threshold/parameters/glossaryRegistry");
+ const { manuallySetSwalTitle } = require("../../threshold/preprocess/gitlabUtils");
+ fetchGlossaryVersion.mockResolvedValue({ version: "2.0" });
+ getGlossaryVersion.mockReturnValue(null);
+ fetchGlossaryData.mockResolvedValue(mockGlossaryData);
+
+ const ref = React.createRef();
+ render(
);
+
+ await ref.current.handleTable(new File(["a,b"], "exp.csv"));
+
+ const titles = swalTitles();
+ // The dialog opened by handleDrop is relabeled to show the glossary download...
+ expect(titles).toContain("Glossary …");
+ // ...before the download starts...
+ const glossaryTitleOrder =
+ manuallySetSwalTitle.mock.invocationCallOrder[titles.indexOf("Glossary …")];
+ const fetchOrder = fetchGlossaryData.mock.invocationCallOrder[0];
+ expect(glossaryTitleOrder).toBeLessThan(fetchOrder);
+ // ...and is restored to "Compiling ..." (never closed) before preprocessing.
+ expect(titles).toContain("Compiling ...");
+ expect(Swal.close).not.toHaveBeenCalled();
+ expect(preprocessExperimentFile).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not relabel to 'Glossary …' when the cached version is current", async () => {
+ const { fetchGlossaryVersion } = require("../components/glossaryApi");
+ const { getGlossaryVersion } = require("../../threshold/parameters/glossaryRegistry");
+ fetchGlossaryVersion.mockResolvedValue({ version: "2.0" });
+ getGlossaryVersion.mockReturnValue("2.0");
+
+ const ref = React.createRef();
+ render();
+
+ await ref.current.handleTable(new File(["a,b"], "exp.csv"));
+
+ // No download, so no glossary status; the shared dialog stays open (never closed).
+ expect(swalTitles()).not.toContain("Glossary …");
+ expect(Swal.close).not.toHaveBeenCalled();
+ });
+
+ it("closes the dialog when the glossary download fails", async () => {
+ const { fetchGlossaryData, fetchGlossaryVersion } = require("../components/glossaryApi");
+ const { getGlossaryVersion } = require("../../threshold/parameters/glossaryRegistry");
+ fetchGlossaryVersion.mockResolvedValue({ version: "2.0" });
+ getGlossaryVersion.mockReturnValue(null);
+ fetchGlossaryData.mockRejectedValue(new Error("network down"));
+ const consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
+
+ const ref = React.createRef();
+ render();
+
+ await ref.current.handleTable(new File(["a,b"], "exp.csv"));
+
+ expect(swalTitles()).toContain("Glossary …");
+ // The error path closes the dialog instead of leaving it spinning forever.
+ expect(Swal.close).toHaveBeenCalledTimes(1);
+ expect(preprocessExperimentFile).not.toHaveBeenCalled();
+
+ consoleError.mockRestore();
+ });
+});
diff --git a/source/__tests__/prolificIntegration.test.js b/source/__tests__/prolificIntegration.test.js
index 197124cf..8a1c9ee6 100644
--- a/source/__tests__/prolificIntegration.test.js
+++ b/source/__tests__/prolificIntegration.test.js
@@ -8,6 +8,11 @@ import {
COMPLETION_CODE_ACTION,
COMPLETION_CODE_TYPE,
} from "../components/prolificConstants";
+import { getGlossary } from "../../threshold/parameters/glossaryRegistry";
+
+jest.mock("../../threshold/parameters/glossaryRegistry", () => ({
+ getGlossary: jest.fn(),
+}));
// Mock global fetch
global.fetch = jest.fn();
@@ -33,6 +38,7 @@ describe("Prolific Integration - New Parameters", () => {
_online2Description: { default: "Default Description" },
},
};
+ getGlossary.mockReturnValue(mockGlossaryData.glossary);
mockInternalName = "TestExperiment";
mockCompletionCode = "COMPLETED123";
mockIncompatibleCode = "INCOMPATIBLE456";
diff --git a/source/components/prolificIntegration.js b/source/components/prolificIntegration.js
index ff91f977..b22e6e23 100644
--- a/source/components/prolificIntegration.js
+++ b/source/components/prolificIntegration.js
@@ -18,6 +18,7 @@ import {
VR_HEADSET_USAGE_PROLIFIC_MAPPING,
} from "./prolificConstants";
import { captureError } from "../sentry";
+import { getGlossary } from "../../threshold/parameters/glossaryRegistry";
const prolificStudySubmissionStatus = {
RESERVED: "RESERVED",
@@ -360,7 +361,6 @@ export const prolificCreateDraft = async (
incompatibleCompletionCode,
abortedCompletionCode,
token,
- glossaryData,
) => {
// const prolificStudyDraftApiUrl = "https://api.prolific.com/api/v1/studies/";
const prolificStudyDraftApiUrl = "/.netlify/functions/prolific/studies/";
@@ -516,7 +516,7 @@ export const prolificCreateDraft = async (
user.currentExperiment.titleOfStudy &&
user.currentExperiment.titleOfStudy !== ""
? user.currentExperiment.titleOfStudy
- : glossaryData.glossary["_online1Title"].default,
+ : getGlossary()["_online1Title"].default,
internal_name:
user.currentExperiment._online1InternalName &&
user.currentExperiment._online1InternalName !== ""
@@ -526,7 +526,7 @@ export const prolificCreateDraft = async (
user.currentExperiment.descriptionOfStudy &&
user.currentExperiment.descriptionOfStudy !== ""
? user.currentExperiment.descriptionOfStudy
- : glossaryData.glossary["_online2Description"].default,
+ : getGlossary()["_online2Description"].default,
external_study_url: user.currentExperiment.experimentUrl,
prolific_id_option: "url_parameters",
completion_option: "url",
diff --git a/threshold b/threshold
index 4083fc2e..a931ae98 160000
--- a/threshold
+++ b/threshold
@@ -1 +1 @@
-Subproject commit 4083fc2eebe754abf05d703ffc8c80641eadc956
+Subproject commit a931ae9865c39176a691120da593ad19c6ddae16