Add hotkey tests, make them pass #69
13 changed files with 1494 additions and 401 deletions
1720
package-lock.json
generated
1720
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -51,7 +51,7 @@
|
|||
"jsdom": "^20.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vitest": "^0.29.3",
|
||||
"vitest": "^1.3.1",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -208,7 +208,7 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
unref(achievement.earned) &&
|
||||
!(
|
||||
display != null &&
|
||||
typeof display == "object" &&
|
||||
typeof display === "object" &&
|
||||
"optionsDisplay" in (display as Record<string, unknown>)
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -92,7 +92,7 @@ export function setDefault<T, K extends keyof T>(
|
|||
key: K,
|
||||
value: T[K]
|
||||
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
||||
if (object[key] === undefined && value != undefined) {
|
||||
if (object[key] == null && value != null) {
|
||||
object[key] = value;
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
|
|||
if (value != null && typeof value === "object") {
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typeof (value as Record<string, any>).type == "symbol" &&
|
||||
typeof (value as Record<string, any>).type === "symbol" &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!types.includes((value as Record<string, any>).type)
|
||||
) {
|
||||
|
|
|
@ -128,7 +128,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
}
|
||||
if (prop != undefined || typeof key === "symbol") {
|
||||
if (prop != null || typeof key === "symbol") {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
||||
}
|
||||
return cache[key].value;
|
||||
} else if (prop != undefined) {
|
||||
} else if (prop != null) {
|
||||
return unref(prop);
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
prop = (target as any)[`on${key}`];
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
} else if (prop != undefined) {
|
||||
} else if (prop != null) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ export function createGrid<T extends GridOptions>(
|
|||
return grid.id + "-" + cell;
|
||||
};
|
||||
grid.getState = function (this: GenericGrid, cell: string | number) {
|
||||
if (this.cellState.value[cell] != undefined) {
|
||||
if (this.cellState.value[cell] != null) {
|
||||
return cellState.value[cell];
|
||||
}
|
||||
return this.cells[cell].startState;
|
||||
|
|
|
@ -99,16 +99,30 @@ document.onkeydown = function (e) {
|
|||
if (hasWon.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
let key = e.key;
|
||||
if (uppercaseNumbers.includes(key)) {
|
||||
key = "shift+" + uppercaseNumbers.indexOf(key);
|
||||
const keysToCheck: string[] = [e.key];
|
||||
if (e.shiftKey && e.ctrlKey) {
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toUpperCase());
|
||||
if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("ctrl+shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
keysToCheck.push("shift+ctrl+" + uppercaseNumbers.indexOf(e.key));
|
||||
} else {
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toLowerCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toLowerCase());
|
||||
}
|
||||
} else if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("shift+" + e.key);
|
||||
keysToCheck.push("shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
} else if (e.shiftKey) {
|
||||
key = "shift+" + key;
|
||||
keysToCheck.push("shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+" + e.key.toLowerCase());
|
||||
} else if (e.ctrlKey) {
|
||||
// remove e.key since the key doesn't change based on ctrl being held or not
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+" + e.key);
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
key = "ctrl+" + key;
|
||||
}
|
||||
const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
|
||||
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
|
||||
if (hotkey && unref(hotkey.enabled)) {
|
||||
e.preventDefault();
|
||||
hotkey.onPress();
|
||||
|
|
|
@ -342,7 +342,7 @@ export const branchedResetPropagation = function (
|
|||
if (links == null) return;
|
||||
const reset: GenericTreeNode[] = [];
|
||||
let current = [resettingNode];
|
||||
while (current.length != 0) {
|
||||
while (current.length !== 0) {
|
||||
const next: GenericTreeNode[] = [];
|
||||
for (const node of current) {
|
||||
for (const link of links.filter(link => link.startNode === node)) {
|
||||
|
|
|
@ -43,7 +43,7 @@ function update() {
|
|||
loadingSave.value = false;
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (player.offlineTime != null) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ function update() {
|
|||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
if (player.devSpeed != null) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export function exponentialFormat(num: DecimalSource, precision: number, mantiss
|
|||
}
|
||||
|
||||
export function commaFormat(num: DecimalSource, precision: number): string {
|
||||
if (num === null || num === undefined) {
|
||||
if (num == null) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
@ -36,12 +36,12 @@ export function commaFormat(num: DecimalSource, precision: number): string {
|
|||
const init = num.toStringWithDecimalPlaces(precision);
|
||||
const portions = init.split(".");
|
||||
portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
|
||||
if (portions.length == 1) return portions[0];
|
||||
if (portions.length === 1) return portions[0];
|
||||
return portions[0] + "." + portions[1];
|
||||
}
|
||||
|
||||
export function regularFormat(num: DecimalSource, precision: number): string {
|
||||
if (num === null || num === undefined) {
|
||||
if (num == null) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
|
|
@ -110,7 +110,7 @@ function syncSaves(
|
|||
}
|
||||
availableSlots.delete(cloudSave.slot);
|
||||
const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
|
||||
if (localSaveId == undefined) {
|
||||
if (localSaveId == null) {
|
||||
settings.saves.push(cloudSave.content.id);
|
||||
save(setupInitialStore(cloudSave.content));
|
||||
} else {
|
||||
|
|
|
@ -191,7 +191,7 @@ export function computeOptionalComponent(
|
|||
watchEffect(() => {
|
||||
const currComponent = unwrapRef(component);
|
||||
comp.value =
|
||||
currComponent == "" || currComponent == null
|
||||
currComponent === "" || currComponent == null
|
||||
? null
|
||||
: coerceComponent(currComponent, defaultWrapper);
|
||||
});
|
||||
|
|
100
tests/features/hotkey.test.ts
Normal file
100
tests/features/hotkey.test.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { createHotkey, hotkeys } from "features/hotkey";
|
||||
import { afterEach, describe, expect, onTestFailed, test } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
function createSuccessHotkey(key: string, triggered: Ref<boolean>) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "",
|
||||
key: key,
|
||||
onPress: () => (triggered.value = true)
|
||||
}));
|
||||
}
|
||||
|
||||
function createFailHotkey(key: string) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "Fail test",
|
||||
key,
|
||||
onPress: () => expect(true).toBe(false)
|
||||
}));
|
||||
}
|
||||
|
||||
function mockKeypress(key: string, shiftKey = false, ctrlKey = false) {
|
||||
const event = new KeyboardEvent("keydown", { key, shiftKey, ctrlKey });
|
||||
expect(document.dispatchEvent(event)).toBe(true);
|
||||
return event;
|
||||
}
|
||||
|
||||
function testHotkey(pass: string, fail: string, key: string, shiftKey = false, ctrlKey = false) {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(pass, triggered);
|
||||
createFailHotkey(fail);
|
||||
mockKeypress(key, shiftKey, ctrlKey);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
|
||||
describe("Hotkeys fire correctly", () => {
|
||||
afterEach(() => {
|
||||
Object.keys(hotkeys).forEach(key => delete hotkeys[key]);
|
||||
});
|
||||
|
||||
test("Lower case letters", () => testHotkey("a", "A", "a"));
|
||||
|
||||
test.each([["A"], ["shift+a"], ["shift+A"]])("Upper case letters using %s as key", key => {
|
||||
testHotkey(key, "a", "A", true);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[0, ")"],
|
||||
[1, "!"],
|
||||
[2, "@"],
|
||||
[3, "#"],
|
||||
[4, "$"],
|
||||
[5, "%"],
|
||||
[6, "^"],
|
||||
[7, "&"],
|
||||
[8, "*"],
|
||||
[9, "("]
|
||||
])("Handle number %i and it's 'capital', %s", (number, symbol) => {
|
||||
test("Triggering number", () =>
|
||||
testHotkey(number.toString(), symbol, number.toString(), true));
|
||||
test.each([symbol, `shift+${number}`, `shift+${symbol}`])(
|
||||
"Triggering symbol using %s as key",
|
||||
key => testHotkey(key, number.toString(), symbol, true)
|
||||
);
|
||||
});
|
||||
|
||||
test("Ctrl modifier", () => testHotkey("ctrl+a", "a", "a", false, true));
|
||||
|
||||
test.each(["shift+ctrl+a", "ctrl+shift+a", "shift+ctrl+A", "ctrl+shift+A"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("a");
|
||||
createFailHotkey("A");
|
||||
createFailHotkey("shift+A");
|
||||
createFailHotkey("shift+a");
|
||||
createFailHotkey("ctrl+a");
|
||||
createFailHotkey("ctrl+A");
|
||||
mockKeypress("a", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(["shift+ctrl+1", "ctrl+shift+1", "shift+ctrl+!", "ctrl+shift+!"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("1");
|
||||
createFailHotkey("!");
|
||||
createFailHotkey("shift+1");
|
||||
createFailHotkey("shift+!");
|
||||
createFailHotkey("ctrl+1");
|
||||
createFailHotkey("ctrl+!");
|
||||
mockKeypress("!", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -6,14 +6,11 @@ interface CustomMatchers<R = unknown> {
|
|||
toLogError(): R;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Vi {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Assertion extends CustomMatchers {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
||||
}
|
||||
declare module "vitest" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Assertion extends CustomMatchers {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
|
|
Loading…
Add table
Reference in a new issue