From 49eb569d0c19dc11d7a03ba8473ad58ec1c0ebdc Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Tue, 16 Jun 2026 16:27:14 -0400 Subject: [PATCH 1/3] ref: extract vocabularies schema URL logic into a separate method in task component --- src/components/task.vue | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/task.vue b/src/components/task.vue index 83915b57..db6a26cc 100644 --- a/src/components/task.vue +++ b/src/components/task.vue @@ -283,10 +283,7 @@ export default { url += `&screen_version=${this.screenVersion}`; } - // For Vocabularies - if (window.ProcessMaker && window.ProcessMaker.packages && window.ProcessMaker.packages.includes('package-vocabularies')) { - window.ProcessMaker.VocabulariesSchemaUrl = `vocabularies/task_schema/${this.taskId}`; - } + this.setVocabulariesSchemaUrl(); return this.beforeLoadTask(this.taskId, this.nodeId).then(() => { this.$dataProvider @@ -693,6 +690,19 @@ export default { return this.$dataProvider.getTasks(`?${queryString}`); }, + setVocabulariesSchemaUrl() { + if ( + window.ProcessMaker + && window.ProcessMaker.packages + && window.ProcessMaker.packages.includes("package-vocabularies") + ) { + const schemaUrl = `vocabularies/task_schema/${this.taskId}`; + if (window.ProcessMaker.VocabulariesSchemaUrl !== schemaUrl) { + window.ProcessMaker.VocabulariesSchemaCache = null; + } + window.ProcessMaker.VocabulariesSchemaUrl = schemaUrl; + } + }, /** * Parses a JSON string and returns the result. * @param {string} jsonString - The JSON string to parse. From 11327d2bc320e4c15314500e3a8daf29a79d8011 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Tue, 16 Jun 2026 16:33:37 -0400 Subject: [PATCH 2/3] feat: implement getVocabulariesSchema method to streamline vocabularies schema retrieval --- src/mixins/ScreenBase.js | 63 ++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/mixins/ScreenBase.js b/src/mixins/ScreenBase.js index bc971dbc..ebaaf56f 100644 --- a/src/mixins/ScreenBase.js +++ b/src/mixins/ScreenBase.js @@ -10,25 +10,56 @@ import { findRootScreen } from "./DataReference"; const stringFormats = ['string', 'datetime', 'date', 'password']; const parentReference = []; +const getVocabulariesSchema = () => { + if ( + window.ProcessMaker && + window.ProcessMaker.packages && + window.ProcessMaker.packages.includes("package-vocabularies") + ) { + if (window.ProcessMaker.VocabulariesSchemaUrl) { + const schemaUrl = window.ProcessMaker.VocabulariesSchemaUrl; + const cache = window.ProcessMaker.VocabulariesSchemaCache; + + if (cache && cache.url === schemaUrl) { + return cache.promise || cache.data; + } + + const promise = window.ProcessMaker.apiClient + .get(schemaUrl) + .then((response) => { + if (window.ProcessMaker.VocabulariesSchemaUrl === schemaUrl) { + window.ProcessMaker.VocabulariesSchemaCache = { + url: schemaUrl, + data: response.data + }; + } + return response.data; + }) + .catch((error) => { + if (window.ProcessMaker.VocabulariesSchemaCache?.url === schemaUrl) { + window.ProcessMaker.VocabulariesSchemaCache = null; + } + throw error; + }); + + window.ProcessMaker.VocabulariesSchemaCache = { + url: schemaUrl, + promise + }; + + return promise; + } + if (window.ProcessMaker.VocabulariesPreview) { + return window.ProcessMaker.VocabulariesPreview; + } + } + return {}; +}; + export default { name: "ScreenContent", mixins: [DataReference, computedFields, VariablesToSubmitFilter], - schema: [ - function() { - if (window.ProcessMaker && window.ProcessMaker.packages && window.ProcessMaker.packages.includes('package-vocabularies')) { - if (window.ProcessMaker.VocabulariesSchemaUrl) { - let response = window.ProcessMaker.apiClient.get(window.ProcessMaker.VocabulariesSchemaUrl); - return response.then(response => { - return response.data; - }); - } - if (window.ProcessMaker.VocabulariesPreview) { - return window.ProcessMaker.VocabulariesPreview; - } - } - return {}; - }, - ], + schema: [getVocabulariesSchema], data() { return { ValidationRules__: {}, From 249c77b13bb5be7bea26921d06a0e5a5526014bd Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Tue, 16 Jun 2026 17:10:00 -0400 Subject: [PATCH 3/3] test: add unit tests for setVocabulariesSchemaUrl and VocabulariesSchema caching behavior --- tests/unit/TaskSelfServiceLock.spec.js | 39 +++++++ tests/unit/VocabulariesSchema.spec.js | 136 +++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 tests/unit/VocabulariesSchema.spec.js diff --git a/tests/unit/TaskSelfServiceLock.spec.js b/tests/unit/TaskSelfServiceLock.spec.js index 257a7f8a..1bc9f27c 100644 --- a/tests/unit/TaskSelfServiceLock.spec.js +++ b/tests/unit/TaskSelfServiceLock.spec.js @@ -133,6 +133,7 @@ describe('Task self-service lock', () => { screenVersion: null, beforeLoadTask: jest.fn().mockResolvedValue(), $dataProvider: { getTasks }, + setVocabulariesSchemaUrl: jest.fn(), setSelfService, linkTask, checkTaskStatus, @@ -150,4 +151,42 @@ describe('Task self-service lock', () => { expect(checkTaskStatus).toHaveBeenCalled(); expect(context.loadingTask).toBe(false); }); + + test("setVocabulariesSchemaUrl invalidates cache when task changes", () => { + sandbox.window.ProcessMaker = { + packages: ["package-vocabularies"], + VocabulariesSchemaUrl: "vocabularies/task_schema/222", + VocabulariesSchemaCache: { + url: "vocabularies/task_schema/222", + data: { current: true } + } + }; + + Task.methods.setVocabulariesSchemaUrl.call({ taskId: 223 }); + + expect(sandbox.window.ProcessMaker.VocabulariesSchemaUrl).toBe( + "vocabularies/task_schema/223" + ); + expect(sandbox.window.ProcessMaker.VocabulariesSchemaCache).toBeNull(); + }); + + test("setVocabulariesSchemaUrl keeps cache when task has not changed", () => { + const cache = { + url: "vocabularies/task_schema/223", + data: { current: true } + }; + + sandbox.window.ProcessMaker = { + packages: ["package-vocabularies"], + VocabulariesSchemaUrl: "vocabularies/task_schema/223", + VocabulariesSchemaCache: cache + }; + + Task.methods.setVocabulariesSchemaUrl.call({ taskId: 223 }); + + expect(sandbox.window.ProcessMaker.VocabulariesSchemaUrl).toBe( + "vocabularies/task_schema/223" + ); + expect(sandbox.window.ProcessMaker.VocabulariesSchemaCache).toBe(cache); + }); }); diff --git a/tests/unit/VocabulariesSchema.spec.js b/tests/unit/VocabulariesSchema.spec.js new file mode 100644 index 00000000..e6361507 --- /dev/null +++ b/tests/unit/VocabulariesSchema.spec.js @@ -0,0 +1,136 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const mixinPath = path.join(process.cwd(), "src/mixins/ScreenBase.js"); + +const source = fs.readFileSync(mixinPath, "utf8"); + +function getScreenBaseOptions() { + const executableScript = source + .replace(/^import .*$/gm, "") + .replace("export default", "module.exports ="); + + const sandbox = { + module: { exports: {} }, + exports: {}, + window: { + ProcessMaker: {} + }, + DataReference: {}, + VariablesToSubmitFilter: {}, + computedFields: {}, + ValidationMsg: {}, + Mustache: { render: jest.fn() }, + get: jest.fn(), + isEqual: jest.fn(), + set: jest.fn(), + debounce: jest.fn(), + findRootScreen: jest.fn(), + mapActions: jest.fn(() => ({})), + mapGetters: jest.fn(() => ({})), + mapState: jest.fn(() => ({})) + }; + + vm.runInNewContext(executableScript, sandbox, { filename: mixinPath }); + + return { + screenBase: sandbox.module.exports, + sandbox + }; +} + +describe("Vocabularies schema cache", () => { + const { screenBase: ScreenBase, sandbox } = getScreenBaseOptions(); + const getVocabulariesSchema = ScreenBase.schema[0]; + + beforeEach(() => { + sandbox.window.ProcessMaker = { + packages: ["package-vocabularies"], + VocabulariesSchemaUrl: "vocabularies/task_schema/123", + apiClient: { + get: jest.fn() + } + }; + }); + + test("shares one in-flight vocabulary schema request for the current task", async () => { + const schema = { properties: { course: { type: "string" } } }; + let resolveRequest; + const request = new Promise((resolve) => { + resolveRequest = resolve; + }); + + sandbox.window.ProcessMaker.apiClient.get.mockReturnValue(request); + + const firstSchema = getVocabulariesSchema(); + const secondSchema = getVocabulariesSchema(); + + expect(secondSchema).toBe(firstSchema); + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenCalledTimes(1); + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenCalledWith( + "vocabularies/task_schema/123" + ); + + resolveRequest({ data: schema }); + await expect(firstSchema).resolves.toBe(schema); + + expect(getVocabulariesSchema()).toBe(schema); + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenCalledTimes(1); + }); + + test("loads a new vocabulary schema once when the task URL changes", async () => { + const firstSchema = { task: 123 }; + const secondSchema = { task: 456 }; + + sandbox.window.ProcessMaker.apiClient.get + .mockResolvedValueOnce({ data: firstSchema }) + .mockResolvedValueOnce({ data: secondSchema }); + + await expect(getVocabulariesSchema()).resolves.toBe(firstSchema); + + sandbox.window.ProcessMaker.VocabulariesSchemaUrl = + "vocabularies/task_schema/456"; + + await expect(getVocabulariesSchema()).resolves.toBe(secondSchema); + + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenCalledTimes(2); + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenNthCalledWith( + 1, + "vocabularies/task_schema/123" + ); + expect(sandbox.window.ProcessMaker.apiClient.get).toHaveBeenNthCalledWith( + 2, + "vocabularies/task_schema/456" + ); + expect(getVocabulariesSchema()).toBe(secondSchema); + }); + + test("does not let a stale task response overwrite the current schema cache", async () => { + let resolveFirstRequest; + const firstRequest = new Promise((resolve) => { + resolveFirstRequest = resolve; + }); + const secondSchema = { task: 456 }; + + sandbox.window.ProcessMaker.apiClient.get + .mockReturnValueOnce(firstRequest) + .mockResolvedValueOnce({ data: secondSchema }); + + const firstSchema = getVocabulariesSchema(); + + sandbox.window.ProcessMaker.VocabulariesSchemaUrl = + "vocabularies/task_schema/456"; + sandbox.window.ProcessMaker.VocabulariesSchemaCache = null; + + await expect(getVocabulariesSchema()).resolves.toBe(secondSchema); + + resolveFirstRequest({ data: { task: 123 } }); + await expect(firstSchema).resolves.toEqual({ task: 123 }); + + expect(sandbox.window.ProcessMaker.VocabulariesSchemaCache).toEqual({ + url: "vocabularies/task_schema/456", + data: secondSchema + }); + }); +});