From a5703dff0858f15c815db9ef155a95747929631b Mon Sep 17 00:00:00 2001 From: Andrew Telnov Date: Wed, 10 Jul 2024 14:29:05 +0300 Subject: [PATCH] The timer is suspended and displays the wrong time if another browser tab was opened during the quiz fix #8509 --- src/base-interfaces.ts | 2 + src/page.ts | 5 ++ src/survey.ts | 19 ++--- src/surveyTaskManager.ts | 7 +- src/surveyTimerModel.ts | 14 ++-- src/surveytimer.ts | 29 +++++-- tests/surveytimertests.ts | 163 +++++++++++++++++++++++++++++++++++--- 7 files changed, 198 insertions(+), 41 deletions(-) diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index 35298377f7..0051417e53 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -150,6 +150,8 @@ export interface ISurvey extends ITextProcessor, ISurveyErrorOwner { maxOthersLength: number; clearValueOnDisableItems: boolean; + maxTimeToFinishPage: number; + uploadFiles( question: IQuestion, name: string, diff --git a/src/page.ts b/src/page.ts index 61fadf8935..caa8f22d10 100644 --- a/src/page.ts +++ b/src/page.ts @@ -256,6 +256,11 @@ export class PageModel extends PanelModelBase implements IPage { public set maxTimeToFinish(val: number) { this.setPropertyValue("maxTimeToFinish", val); } + public getMaxTimeToFinish(): number { + if(this.maxTimeToFinish !== 0) return this.maxTimeToFinish; + const res = !!this.survey ? this.survey.maxTimeToFinishPage : 0; + return res > 0 ? res : 0; + } protected onNumChanged(value: number) { } protected onVisibleChanged() { if (this.isRandomizing) return; diff --git a/src/survey.ts b/src/survey.ts index 5931895545..5b296a7600 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -4415,7 +4415,7 @@ export class SurveyModel extends SurveyElementCore private calcIsShowPrevButton(): boolean { if (this.isFirstPage || !this.showPrevButton || this.state !== "running") return false; var page = this.visiblePages[this.currentPageNo - 1]; - return this.getPageMaxTimeToFinish(page) <= 0; + return page && page.getMaxTimeToFinish() <= 0; } private calcIsShowNextButton(): boolean { return this.state === "running" && !this.isLastPage && !this.canBeCompletedByTrigger; @@ -7183,7 +7183,7 @@ export class SurveyModel extends SurveyElementCore if (!page) return { spent: 0, limit: 0 }; let pageSpent = page.timeSpent; let surveySpent = this.timeSpent; - let pageLimitSec = this.getPageMaxTimeToFinish(page); + let pageLimitSec = page.getMaxTimeToFinish(); let surveyLimit = this.maxTimeToFinish; if (this.showTimerPanelMode == "page") { return { spent: pageSpent, limit: pageLimitSec }; @@ -7210,7 +7210,7 @@ export class SurveyModel extends SurveyElementCore if (!page) return ""; var pageSpent = this.getDisplayTime(page.timeSpent); var surveySpent = this.getDisplayTime(this.timeSpent); - var pageLimitSec = this.getPageMaxTimeToFinish(page); + var pageLimitSec = page.getMaxTimeToFinish(); var pageLimit = this.getDisplayTime(pageLimitSec); var surveyLimit = this.getDisplayTime(this.maxTimeToFinish); if (this.showTimerPanelMode == "page") @@ -7243,7 +7243,7 @@ export class SurveyModel extends SurveyElementCore pageSpent: string, pageLimit: string ): string { - return this.getPageMaxTimeToFinish(page) > 0 + return !!page && page.getMaxTimeToFinish() > 0 ? this.getLocalizationFormatString("timerLimitPage", pageSpent, pageLimit) : this.getLocalizationFormatString("timerSpentPage", pageSpent, pageLimit); } @@ -7353,19 +7353,14 @@ export class SurveyModel extends SurveyElementCore public set maxTimeToFinishPage(val: number) { this.setPropertyValue("maxTimeToFinishPage", val); } - private getPageMaxTimeToFinish(page: PageModel) { - if (!page || page.maxTimeToFinish < 0) return 0; - return page.maxTimeToFinish > 0 - ? page.maxTimeToFinish - : this.maxTimeToFinishPage; - } private doTimer(page: PageModel): void { this.onTimer.fire(this, {}); - if (this.maxTimeToFinish > 0 && this.maxTimeToFinish == this.timeSpent) { + if (this.maxTimeToFinish > 0 && this.maxTimeToFinish <= this.timeSpent) { + this.timeSpent = this.maxTimeToFinish; this.completeLastPage(); } if (page) { - var pageLimit = this.getPageMaxTimeToFinish(page); + var pageLimit = page.getMaxTimeToFinish(); if (pageLimit > 0 && pageLimit == page.timeSpent) { if (this.isLastPage) { this.completeLastPage(); diff --git a/src/surveyTaskManager.ts b/src/surveyTaskManager.ts index ebd2590796..cf8824d6a1 100644 --- a/src/surveyTaskManager.ts +++ b/src/surveyTaskManager.ts @@ -1,10 +1,5 @@ -import { ISurvey } from "./base-interfaces"; import { Base, EventBase } from "./base"; -import { SurveyTimer } from "./surveytimer"; import { property } from "./jsonobject"; -import { PageModel } from "./page"; -import { SurveyModel } from "./survey"; -import { CssClassBuilder } from "./utils/cssClassBuilder"; class SurveyTaskModel { private timestamp: Date; @@ -31,7 +26,7 @@ export class SurveyTaskManagerModel extends Base { return task; } - public waitAndExecute(action: any) { + public waitAndExecute(action: any): void { if(!this.hasActiveTasks) { action(); return; diff --git a/src/surveyTimerModel.ts b/src/surveyTimerModel.ts index 7fc7fb2024..16cf2b9c35 100644 --- a/src/surveyTimerModel.ts +++ b/src/surveyTimerModel.ts @@ -1,6 +1,6 @@ import { ISurvey } from "./base-interfaces"; import { Base, EventBase } from "./base"; -import { SurveyTimer } from "./surveytimer"; +import { SurveyTimer, SurveyTimerEvent } from "./surveytimer"; import { property } from "./jsonobject"; import { PageModel } from "./page"; import { SurveyModel } from "./survey"; @@ -38,7 +38,7 @@ export class SurveyTimerModel extends Base { this.survey.onCurrentPageChanged.add(() => { this.update(); }); - this.timerFunc = (): void => { this.doTimer(); }; + this.timerFunc = (sender: SurveyTimer, options: SurveyTimerEvent): void => { this.doTimer(options.seconds); }; this.setIsRunning(true); this.update(); SurveyTimer.instance.start(this.timerFunc); @@ -58,12 +58,16 @@ export class SurveyTimerModel extends Base { this.updateText(); this.updateProgress(); } - private doTimer(): void { + private doTimer(seconds: number): void { var page = (this.survey).currentPage; if (page) { - page.timeSpent = page.timeSpent + 1; + const pageMaxTime = page.getMaxTimeToFinish(); + if(pageMaxTime > 0 && pageMaxTime < page.timeSpent + seconds) { + seconds = pageMaxTime - page.timeSpent; + } + page.timeSpent = page.timeSpent + seconds; } - this.spent = this.spent + 1; + this.spent = this.spent + seconds; this.update(); if (this.onTimer) { this.onTimer(page); diff --git a/src/surveytimer.ts b/src/surveytimer.ts index d62078a639..101ea08cfe 100644 --- a/src/surveytimer.ts +++ b/src/surveytimer.ts @@ -1,4 +1,4 @@ -import { Event } from "./base"; +import { EventBase } from "./base"; export var surveyTimerFunctions = { setTimeout: (func: () => any): number => { @@ -14,12 +14,17 @@ export var surveyTimerFunctions = { } else { return setTimeout(func, delay); } - } + }, + now(): number { return Date.now(); } }; +export interface SurveyTimerEvent { + seconds: number; +} + export class SurveyTimer { private static instanceValue: SurveyTimer = null; - public static get instance() { + public static get instance(): SurveyTimer { if (!SurveyTimer.instanceValue) { SurveyTimer.instanceValue = new SurveyTimer(); } @@ -27,11 +32,13 @@ export class SurveyTimer { } private listenerCounter = 0; private timerId = -1; - public onTimer: Event<() => any, SurveyTimer, any> = new Event<() => any, SurveyTimer, any>(); - public start(func: () => any = null) { + private prevTimeInMs: number; + public onTimer: EventBase = new EventBase(); + public start(func: (timer: SurveyTimer, options: SurveyTimerEvent) => void = null): void { if (func) { this.onTimer.add(func); } + this.prevTimeInMs = surveyTimerFunctions.now(); if (this.timerId < 0) { this.timerId = surveyTimerFunctions.setTimeout(() => { this.doTimer(); @@ -39,7 +46,7 @@ export class SurveyTimer { } this.listenerCounter++; } - public stop(func: () => any = null) { + public stop(func: (timer: SurveyTimer, options: SurveyTimerEvent) => any = null): void { if (func) { this.onTimer.remove(func); } @@ -49,13 +56,19 @@ export class SurveyTimer { this.timerId = -1; } } - public doTimer() { + public doTimer(): void { if(this.onTimer.isEmpty || this.listenerCounter == 0) { this.timerId = -1; } if (this.timerId < 0) return; + const newTimer = surveyTimerFunctions.now(); + let seconds = Math.floor((newTimer - this.prevTimeInMs) / 1000); + this.prevTimeInMs = newTimer; + if(seconds < 0) { + seconds = 1; + } const prevItem = this.timerId; - this.onTimer.fire(this, {}); + this.onTimer.fire(this, { seconds: seconds }); //We have to check that we have the same timerId //It could be changed during events execution and it will lead to double timer events if(prevItem !== this.timerId) return; diff --git a/tests/surveytimertests.ts b/tests/surveytimertests.ts index 17226e18da..8017854eb4 100644 --- a/tests/surveytimertests.ts +++ b/tests/surveytimertests.ts @@ -1,7 +1,5 @@ -import { PageModel } from "../src/page"; import { SurveyModel } from "../src/survey"; -import { SurveyTimer, surveyTimerFunctions } from "../src/surveytimer"; -import { SurveyTimerModel } from "../src/surveyTimerModel"; +import { SurveyTimer, surveyTimerFunctions, SurveyTimerEvent } from "../src/surveytimer"; import { defaultV2Css } from "../src/defaultCss/defaultV2Css"; export default QUnit.module("SurveyTimer"); @@ -10,10 +8,12 @@ surveyTimerFunctions.setTimeout = function(func: () => any): number { return 1; }; surveyTimerFunctions.clearTimeout = function(timerId: number) {}; +let nowValue = 1000000000; +surveyTimerFunctions.now = function(): number { nowValue += 1000; return nowValue; }; QUnit.test("Test timer event", function(assert) { var counter = 0; - var func = function() { + var func = function(sender: SurveyTimer, event: SurveyTimerEvent) { counter++; }; SurveyTimer.instance.start(func); @@ -24,6 +24,25 @@ QUnit.test("Test timer event", function(assert) { assert.equal(counter, 5, "Timer was stopped nothing happened"); }); +function doTimer(count: number, suspendedSeconds: number = 0) { + nowValue += suspendedSeconds * 1000; + for (var i = 0; i < count; i++) { + SurveyTimer.instance.doTimer(); + } +} + +QUnit.test("Test suspended timer event", function(assert) { + var seconds = 0; + var func = function(sender: SurveyTimer, options: SurveyTimerEvent) { + seconds += options.seconds; + }; + SurveyTimer.instance.start(func); + doTimer(5); + assert.equal(seconds, 5, "#1"); + doTimer(1, 7); + assert.equal(seconds, 5 + 1 + 7, "#2"); +}); + QUnit.test("Spent time on survey", function(assert) { var survey = new SurveyModel(); survey.startTimer(); @@ -46,6 +65,28 @@ QUnit.test("Spent time on survey", function(assert) { survey.stopTimer(); }); +QUnit.test("Spent time on survey with suspended timer", function(assert) { + var survey = new SurveyModel(); + survey.startTimer(); + assert.equal(survey.timeSpent, 0, "Timer was not started"); + doTimer(1, 4); + assert.equal(survey.timeSpent, 5, "Timer called 5 times"); + doTimer(1, 4); + assert.equal(survey.timeSpent, 10, "Timer called 10 times"); + survey.stopTimer(); + doTimer(1, 4); + assert.equal(survey.timeSpent, 10, "Timer still called 10 times"); + survey.startTimer(); + doTimer(1, 4); + assert.equal(survey.timeSpent, 15, "Timer called 15 times"); + survey.doComplete(); + doTimer(1, 4); + assert.equal(survey.timeSpent, 15, "Timer called still 15 times"); + survey.clear(); + assert.equal(survey.timeSpent, 0, "reset value"); + survey.stopTimer(); +}); + QUnit.test("Spent time on pages", function(assert) { var survey = new SurveyModel(); var page1 = survey.addNewPage(); @@ -87,6 +128,74 @@ QUnit.test("Spent time on pages", function(assert) { survey.stopTimer(); }); +QUnit.test("Spent time on pages with suspended timer, #1", function(assert) { + var survey = new SurveyModel(); + var page1 = survey.addNewPage(); + var page2 = survey.addNewPage(); + page1.addNewQuestion("text"); + page2.addNewQuestion("text"); + page1.maxTimeToFinish = 9; + page2.maxTimeToFinish = 8; + survey.startTimer(); + assert.equal(page1.timeSpent, 0, "page1.timeSpent #1"); + assert.equal(survey.timeSpent, 0, "survey.timeSpent #1"); + doTimer(1, 4); + assert.equal(page1.timeSpent, 5, "page1.timeSpent #2"); + assert.equal(survey.timeSpent, 5, "survey.timeSpent #2"); + doTimer(1, 10); + assert.equal(page1.timeSpent, 9, "page1.timeSpent #3"); + assert.equal(survey.timeSpent, 9, "survey.timeSpent #3"); + assert.equal(survey.currentPageNo, 1, "survey.currentPageNo #1"); + assert.equal(page2.timeSpent, 0, "page2, timeSpent #1"); + doTimer(1, 20); + assert.equal(page2.timeSpent, 8, "page2, timeSpent #2"); + assert.equal(survey.timeSpent, 17, "survey.timeSpent #4"); + assert.equal(survey.state, "completed", "The survey is completed"); + survey.stopTimer(); +}); +QUnit.test("Spent time on pages with suspended timer, #2", function(assert) { + var survey = new SurveyModel(); + survey.maxTimeToFinish = 15; + var page1 = survey.addNewPage(); + var page2 = survey.addNewPage(); + page1.addNewQuestion("text"); + page2.addNewQuestion("text"); + page1.maxTimeToFinish = 9; + page2.maxTimeToFinish = 8; + survey.startTimer(); + assert.equal(page1.timeSpent, 0, "page1.timeSpent #1"); + assert.equal(survey.timeSpent, 0, "survey.timeSpent #1"); + doTimer(1); + doTimer(1); + doTimer(1, 40); + assert.equal(page1.timeSpent, 9, "page1.timeSpent #2"); + assert.equal(survey.timeSpent, 9, "survey.timeSpent #2"); + assert.equal(survey.currentPageNo, 1, "survey.currentPageNo #1"); + assert.equal(page2.timeSpent, 0, "page2, timeSpent #1"); + assert.equal(survey.state, "running", "The survey is completed"); + survey.stopTimer(); +}); +QUnit.test("Spent time on pages with suspended timer, #3", function(assert) { + var survey = new SurveyModel(); + survey.maxTimeToFinish = 15; + survey.maxTimeToFinishPage = 10; + var page1 = survey.addNewPage(); + var page2 = survey.addNewPage(); + page1.addNewQuestion("text"); + page2.addNewQuestion("text"); + survey.startTimer(); + assert.equal(page1.timeSpent, 0, "page1.timeSpent #1"); + assert.equal(survey.timeSpent, 0, "survey.timeSpent #1"); + doTimer(1); + doTimer(1); + doTimer(1, 40); + assert.equal(page1.timeSpent, 10, "page1.timeSpent #2"); + assert.equal(survey.timeSpent, 10, "survey.timeSpent #2"); + assert.equal(survey.currentPageNo, 1, "survey.currentPageNo #1"); + assert.equal(page2.timeSpent, 0, "page2, timeSpent #1"); + assert.equal(survey.state, "running", "The survey is completed"); + survey.stopTimer(); +}); QUnit.test("Complete survey by timer", function(assert) { var survey = new SurveyModel(); survey.addNewPage(); @@ -105,6 +214,24 @@ QUnit.test("Complete survey by timer", function(assert) { survey.stopTimer(); }); +QUnit.test("Complete survey by timer with suspended", function(assert) { + var survey = new SurveyModel(); + survey.addNewPage(); + survey.addNewPage(); + survey.pages[0].addNewQuestion("text"); + survey.pages[1].addNewQuestion("text"); + survey.maxTimeToFinish = 10; + survey.startTimer(); + assert.equal(survey.state, "running", "The state is running"); + doTimer(1, 4); + assert.equal(survey.state, "running", "The state is still running"); + assert.equal(survey.timeSpent, 5, "Timer called 5 times"); + doTimer(1, 5); + assert.equal(survey.state, "completed", "The state is completed"); + assert.equal(survey.timeSpent, 10, "Timer called 10 times"); + survey.stopTimer(); +}); + QUnit.test("Complete pages by timer", function(assert) { var survey = new SurveyModel(); survey.addNewPage("p1"); @@ -127,6 +254,28 @@ QUnit.test("Complete pages by timer", function(assert) { survey.stopTimer(); }); +QUnit.test("Complete pages by timer with suspended", function(assert) { + var survey = new SurveyModel(); + survey.addNewPage("p1"); + survey.addNewPage("p2"); + survey.pages[0].addNewQuestion("text"); + survey.pages[1].addNewQuestion("text"); + survey.maxTimeToFinishPage = 10; + survey.pages[1].maxTimeToFinish = 5; + survey.startTimer(); + assert.equal(survey.state, "running", "The state is running"); + assert.equal(survey.currentPage.name, "p1", "The first page"); + doTimer(1, 4); + assert.equal(survey.state, "running", "The state is still running"); + assert.equal(survey.currentPage.name, "p1", "The first page"); + doTimer(1, 4); + assert.equal(survey.state, "running", "The state is still running"); + assert.equal(survey.currentPage.name, "p2", "The second first page"); + doTimer(1, 4); + assert.equal(survey.state, "completed", "The survey is completed"); + survey.stopTimer(); +}); + QUnit.test("Showing prev button", function(assert) { var survey = new SurveyModel(); survey.addNewPage("p1"); @@ -271,12 +420,6 @@ QUnit.test("Allow to modify timeSpent property", (assert) => { survey.stopTimer(); }); -function doTimer(count: number) { - for (var i = 0; i < count; i++) { - SurveyTimer.instance.doTimer(); - } -} - QUnit.test("Test SurveyTimerModel", function(assert) { const survey = new SurveyModel(); survey.addNewPage("p1");