Skip to content

Commit e5b5a57

Browse files
committed
test: integ tests for line height theme setting
1 parent ad96ba7 commit e5b5a57

File tree

4 files changed

+304
-3
lines changed

4 files changed

+304
-3
lines changed

src/extensions/default/DebugCommands/MacroRunner.js

Lines changed: 237 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ define(function (require, exports, module) {
5151
Commands = brackets.getModule("command/Commands"),
5252
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
5353
Editor = brackets.getModule("editor/Editor"),
54+
Dialogs = brackets.getModule("widgets/Dialogs"),
5455
_ = brackets.getModule("thirdparty/lodash"),
5556
ProjectManager = brackets.getModule("project/ProjectManager");
5657

@@ -312,6 +313,12 @@ define(function (require, exports, module) {
312313
}
313314
}
314315

316+
function validateNotEqual(obj1, obj2) {
317+
if(_.isEqual(obj1, obj2)){
318+
throw new Error(`validateEqual: expected ${JSON.stringify(obj1)} to NOT equal ${JSON.stringify(obj2)}`);
319+
}
320+
}
321+
315322
/**
316323
* validates if the given mark type is present in the specified selections
317324
* @param {string} markType
@@ -343,8 +350,8 @@ define(function (require, exports, module) {
343350
return jsPromise(CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }));
344351
}
345352

346-
function execCommand(commandID, args) {
347-
return jsPromise(CommandManager.execute(commandID, args));
353+
function execCommand(commandID, arg) {
354+
return jsPromise(CommandManager.execute(commandID, arg));
348355
}
349356

350357
function undo() {
@@ -383,9 +390,236 @@ define(function (require, exports, module) {
383390
}
384391
};
385392

393+
/**
394+
* Waits for a polling function to succeed or until a timeout is reached.
395+
* The polling function is periodically invoked to check for success, and
396+
* the function rejects with a timeout message if the timeout duration elapses.
397+
*
398+
* @param {function} pollFn - A function that returns `true` or a promise resolving to `true`/`false`
399+
* to indicate success and stop waiting.
400+
* The function will be called repeatedly until it succeeds or times out.
401+
* @param {string|function} _timeoutMessageOrMessageFn - A helpful string message or an async function
402+
* that returns a string message to reject with in case of timeout.
403+
* Example:
404+
* - String: "Condition not met within the allowed time."
405+
* - Function: `async () => "Timeout while waiting for the process to complete."`
406+
* @param {number} [timeoutms=2000] - The maximum time to wait in milliseconds before timing out. Defaults to 2 seconds.
407+
* @param {number} [pollInterval=10] - The interval in milliseconds at which `pollFn` is invoked. Defaults to 10ms.
408+
* @returns {Promise<void>} A promise that resolves when `pollFn` succeeds or rejects with a timeout message.
409+
*
410+
* @throws {Error} If `timeoutms` or `pollInterval` is not a number.
411+
*
412+
* @example
413+
* // Example 1: Using a string as the timeout message
414+
* awaitsFor(
415+
* () => document.getElementById("element") !== null,
416+
* "Element did not appear within the allowed time.",
417+
* 5000,
418+
* 100
419+
* ).then(() => {
420+
* console.log("Element appeared!");
421+
* }).catch(err => {
422+
* console.error(err.message);
423+
* });
424+
*
425+
* @example
426+
* // Example 2: Using a function as the timeout message
427+
* awaitsFor(
428+
* () => document.getElementById("element") !== null,
429+
* async () => {
430+
* const el = document.getElementById("element");
431+
* return `expected ${el} to be null`;
432+
* },
433+
* 10000,
434+
* 500
435+
* ).then(() => {
436+
* console.log("Element appeared!");
437+
* }).catch(err => {
438+
* console.error(err.message);
439+
* });
440+
*/
441+
function awaitsFor(pollFn, _timeoutMessageOrMessageFn, timeoutms = 2000, pollInterval = 10){
442+
if(typeof _timeoutMessageOrMessageFn === "number"){
443+
timeoutms = _timeoutMessageOrMessageFn;
444+
pollInterval = timeoutms;
445+
}
446+
if(!(typeof timeoutms === "number" && typeof pollInterval === "number")){
447+
throw new Error("awaitsFor: invalid parameters when awaiting for " + _timeoutMessageOrMessageFn);
448+
}
449+
450+
async function _getExpectMessage(_timeoutMessageOrMessageFn) {
451+
try{
452+
if(typeof _timeoutMessageOrMessageFn === "function") {
453+
_timeoutMessageOrMessageFn = _timeoutMessageOrMessageFn();
454+
if(_timeoutMessageOrMessageFn instanceof Promise){
455+
_timeoutMessageOrMessageFn = await _timeoutMessageOrMessageFn;
456+
}
457+
}
458+
} catch (e) {
459+
_timeoutMessageOrMessageFn = "Error executing expected message function:" + e.stack;
460+
}
461+
return _timeoutMessageOrMessageFn;
462+
}
463+
464+
function _timeoutPromise(promise, ms) {
465+
const timeout = new Promise((_, reject) => {
466+
setTimeout(async () => {
467+
_timeoutMessageOrMessageFn = await _getExpectMessage(_timeoutMessageOrMessageFn);
468+
reject(new Error(_timeoutMessageOrMessageFn || `Promise timed out after ${ms}ms`));
469+
}, ms);
470+
});
471+
472+
return Promise.race([promise, timeout]);
473+
}
474+
475+
return new Promise((resolve, reject)=>{
476+
let startTime = Date.now(),
477+
lapsedTime;
478+
async function pollingFn() {
479+
try{
480+
let result = pollFn();
481+
482+
// If pollFn returns a promise, await it
483+
if (Object.prototype.toString.call(result) === "[object Promise]") {
484+
// we cant simply check for result instanceof Promise as the Promise may be returned from
485+
// an iframe and iframe has a different instance of Promise than this js context.
486+
result = await _timeoutPromise(result, timeoutms);
487+
}
488+
489+
if (result) {
490+
resolve();
491+
return;
492+
}
493+
lapsedTime = Date.now() - startTime;
494+
if(lapsedTime>timeoutms){
495+
_timeoutMessageOrMessageFn = await _getExpectMessage(_timeoutMessageOrMessageFn);
496+
reject("awaitsFor timed out waiting for - " + _timeoutMessageOrMessageFn);
497+
return;
498+
}
499+
setTimeout(pollingFn, pollInterval);
500+
} catch (e) {
501+
reject(e);
502+
}
503+
}
504+
pollingFn();
505+
});
506+
}
507+
508+
async function waitForModalDialog(dialogClass, friendlyName, timeout = 2000) {
509+
dialogClass = dialogClass || "";
510+
friendlyName = friendlyName || dialogClass || "Modal Dialog";
511+
await awaitsFor(()=>{
512+
let $dlg = $(`.modal.instance${dialogClass}`);
513+
return $dlg.length >= 1;
514+
}, `Waiting for Modal Dialog to show ${friendlyName}`, timeout);
515+
}
516+
517+
async function waitForModalDialogClosed(dialogClass, friendlyName, timeout = 2000) {
518+
dialogClass = dialogClass || "";
519+
friendlyName = friendlyName || dialogClass || "Modal Dialog";
520+
await awaitsFor(()=>{
521+
let $dlg = $(`.modal.instance${dialogClass}`);
522+
return $dlg.length === 0;
523+
}, `Waiting for Modal Dialog to not there ${friendlyName}`, timeout);
524+
}
525+
526+
/** Clicks on a button within a specified dialog.
527+
* This function identifies a dialog using its class and locates a button either by its selector or button ID.
528+
* Validation to ensure the dialog and button exist and that the button is enabled before attempting to click.
529+
*
530+
* @param {string} selectorOrButtonID - The selector or button ID to identify the button to be clicked.
531+
* Example (as selector): ".my-button-class".
532+
* Example (as button ID): "ok".
533+
* @param {string} dialogClass - The class of the dialog (optional). If omitted, defaults to an empty string.
534+
* Example: "my-dialog-class".
535+
* @param {boolean} isButtonID - If `true`, `selectorOrButtonid` is treated as a button ID.
536+
* If `false`, it is treated as a jQuery selector. Default is `false`.
537+
*
538+
* @throws {Error} Throws an error if:
539+
* - The specified dialog does not exist.
540+
* - Multiple buttons match the given selector or ID.
541+
* - No button matches the given selector or ID.
542+
* - The button is disabled and cannot be clicked.
543+
*
544+
*/
545+
function _clickDialogButtonWithSelector(selectorOrButtonID, dialogClass, isButtonID) {
546+
dialogClass = dialogClass || "";
547+
const $dlg = $(`.modal.instance${dialogClass}`);
548+
549+
if(!$dlg.length){
550+
throw new Error(`No such dialog present: "${dialogClass}"`);
551+
}
552+
553+
const $button = isButtonID ?
554+
$dlg.find(".dialog-button[data-button-id='" + selectorOrButtonID + "']") :
555+
$dlg.find(selectorOrButtonID);
556+
if($button.length > 1){
557+
throw new Error(`Multiple button in dialog "${selectorOrButtonID}"`);
558+
} else if(!$button.length){
559+
throw new Error(`No such button in dialog "${selectorOrButtonID}"`);
560+
}
561+
562+
if($button.prop("disabled")) {
563+
throw new Error(`Cannot click, button is disabled. "${selectorOrButtonID}"`);
564+
}
565+
566+
$button.click();
567+
}
568+
569+
/**
570+
* Clicks on a button within a specified dialog using its button ID.
571+
*
572+
* @param {string} buttonID - The unique ID of the button to be clicked. usually One of the
573+
* __PR.Dialogs.DIALOG_BTN_* symbolic constants or a custom id. You can find the button
574+
* id in the dialog by inspecting the button and checking its `data-button-id` attribute
575+
* Example: __PR.Dialogs.DIALOG_BTN_OK.
576+
* @param {string} [dialogClass] - The class of the dialog containing the button. Optional, if only one dialog
577+
* is present, you can omit this.
578+
* Example: "my-dialog-class".
579+
* @throws {Error} Throws an error if:
580+
* - The specified dialog does not exist.
581+
* - No button matches the given button ID.
582+
* - Multiple buttons match the given button ID.
583+
* - The button is disabled and cannot be clicked.
584+
*
585+
* @example
586+
* // Example: Click a button by its ID
587+
* __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK, "my-dialog-class");
588+
* __PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_OK); // if only 1 dialog is present, can omit the dialog class
589+
* __PR.clickDialogButtonID("customBtnID", "my-dialog-class");
590+
*/
591+
function clickDialogButtonID(buttonID, dialogClass) {
592+
_clickDialogButtonWithSelector(buttonID, dialogClass, true);
593+
}
594+
595+
/**
596+
* Clicks on a button within a specified dialog using a selector.
597+
*
598+
* @param {string} buttonSelector - A jQuery selector to identify the button to be clicked.
599+
* Example: ".showImageBtn".
600+
* @param {string} [dialogClass] - The class of the dialog containing the button. Optional, if only one dialog
601+
* is present, you can omit this.
602+
* Example: "my-dialog-class".
603+
* @throws {Error} Throws an error if:
604+
* - The specified dialog does not exist.
605+
* - No button matches the given selector.
606+
* - Multiple buttons match the given selector.
607+
* - The button is disabled and cannot be clicked.
608+
*
609+
* @example
610+
* // Example: Click a button using a selector
611+
* __PR.clickDialogButton(".showImageBtn", "my-dialog-class");
612+
* __PR.clickDialogButton(".showImageBtn"); // if only 1 dialog is present, can omit the dialog class
613+
*/
614+
function clickDialogButton(buttonSelector, dialogClass) {
615+
_clickDialogButtonWithSelector(buttonSelector, dialogClass, false);
616+
}
617+
386618
const __PR= {
387619
openFile, setCursors, expectCursorsToBe, keydown, typeAtCursor, validateText, validateAllMarks, validateMarks,
388-
closeFile, closeAll, undo, redo, setPreference, getPreference, validateEqual, EDITING
620+
closeFile, closeAll, undo, redo, setPreference, getPreference, validateEqual, validateNotEqual, execCommand,
621+
awaitsFor, waitForModalDialog, waitForModalDialogClosed, clickDialogButtonID, clickDialogButton,
622+
EDITING, $, Commands, Dialogs
389623
};
390624

391625
async function runMacro(macroText) {

src/main.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ window.catchToNull = function (promise, logError) {
5959
});
6060
};
6161

62+
/**
63+
* global util to wait for a period of time
64+
* @param waitTimeMs - max time to wait for in ms.
65+
* @returns {Promise}
66+
*/
67+
window.delay = function (waitTimeMs){
68+
return new Promise((resolve)=>{
69+
setTimeout(resolve, waitTimeMs);
70+
});
71+
};
72+
6273
// splash screen updates for initial install which could take time, or slow networks.
6374
let trackedScriptCount = 0;
6475
function _setSplashScreenStatusUpdate(message1, message2) {

test/SpecRunner.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ function awaits(waitTimeMs){
191191
window.jsPromise = jsPromise;
192192
window.awaitsFor = awaitsFor;
193193
window.awaits = awaits;
194+
window.delay = awaits;
194195
/**
195196
* A safe way to return null on promise fail. This will never reject or throw.
196197
* @param promise

test/spec/Generic-integ-test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ define(function (require, exports, module) {
3030
MainViewManager, // loaded from brackets.test
3131
CommandManager,
3232
Commands,
33+
__PR,
3334
SpecRunnerUtils = require("spec/SpecRunnerUtils");
3435

3536

@@ -48,6 +49,7 @@ define(function (require, exports, module) {
4849
MainViewManager = testWindow.brackets.test.MainViewManager;
4950
CommandManager = testWindow.brackets.test.CommandManager;
5051
Commands = testWindow.brackets.test.Commands;
52+
__PR = testWindow.__PR;
5153
}, 30000);
5254

5355
afterAll(async function () {
@@ -58,6 +60,7 @@ define(function (require, exports, module) {
5860
MainViewManager = null;
5961
CommandManager = null;
6062
Commands = null;
63+
__PR = null;
6164
await SpecRunnerUtils.closeTestWindow();
6265
}, 30000);
6366

@@ -135,6 +138,58 @@ define(function (require, exports, module) {
135138
});
136139
});
137140

141+
describe("Theme settings", function () {
142+
let currentProjectPath;
143+
beforeAll(async function () {
144+
currentProjectPath = await SpecRunnerUtils.getTestPath("/spec/EditorOptionHandlers-test-files");
145+
await SpecRunnerUtils.loadProjectInTestWindow(currentProjectPath);
146+
await _openProjectFile("test.html");
147+
});
148+
149+
it("should preview line height changes on slider input and restore original on cancel", async function () {
150+
await __PR.execCommand(__PR.Commands.CMD_THEMES_OPEN_SETTINGS);
151+
await __PR.waitForModalDialog(".themeSettings");
152+
const currentLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
153+
__PR.$('.fontLineHeightSlider').val(2).trigger('input');
154+
let newLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
155+
__PR.validateNotEqual(currentLineHeight, newLineHeight);
156+
157+
__PR.clickDialogButtonID(__PR.Dialogs.DIALOG_BTN_CANCEL);
158+
await __PR.waitForModalDialogClosed(".themeSettings");
159+
await __PR.awaitsFor(()=>{
160+
const lineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
161+
return currentLineHeight === lineHeight;
162+
}, "Waiting for font size to be restored on cancel");
163+
});
164+
165+
it("should save and apply line height changes", async function () {
166+
await __PR.execCommand(__PR.Commands.CMD_THEMES_OPEN_SETTINGS);
167+
await __PR.waitForModalDialog(".themeSettings");
168+
const originalLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
169+
const originalVal = __PR.$('.fontLineHeightSlider').val();
170+
__PR.$('.fontLineHeightSlider').val(2).trigger('input');
171+
let newLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
172+
__PR.validateNotEqual(originalLineHeight, newLineHeight);
173+
__PR.clickDialogButtonID("save");
174+
await __PR.waitForModalDialogClosed(".themeSettings");
175+
176+
// now open theme settings again and restore old line height
177+
await __PR.execCommand(__PR.Commands.CMD_THEMES_OPEN_SETTINGS);
178+
await __PR.waitForModalDialog(".themeSettings");
179+
__PR.validateEqual(__PR.$('.fontLineHeightSlider').val(), "2");
180+
__PR.$('.fontLineHeightSlider').val(originalVal).trigger('input');
181+
newLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
182+
__PR.validateEqual(originalLineHeight, newLineHeight);
183+
__PR.clickDialogButtonID("save");
184+
await __PR.waitForModalDialogClosed(".themeSettings");
185+
186+
await __PR.awaitsFor(()=>{
187+
newLineHeight = getComputedStyle(__PR.$(".CodeMirror-scroll")[0]).lineHeight;
188+
return originalLineHeight === newLineHeight;
189+
}, "Waiting for font size to be restored on cancel");
190+
});
191+
});
192+
138193
describe("reopen closed files test", function () {
139194
let currentProjectPath;
140195
beforeAll(async function () {

0 commit comments

Comments
 (0)