Add hotkey tests, make them pass #69

Merged
thepaperpilot merged 3 commits from thepaperpilot/Profectus:fix/hotkeys into main 2024-03-29 05:24:09 +00:00
13 changed files with 1494 additions and 401 deletions

1720
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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": {

View file

@ -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>)
)
) {

View file

@ -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)
) {

View file

@ -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;

View file

@ -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();

View file

@ -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)) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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 {

View file

@ -191,7 +191,7 @@ export function computeOptionalComponent(
watchEffect(() => {
const currComponent = unwrapRef(component);
comp.value =
currComponent == "" || currComponent == null
currComponent === "" || currComponent == null
? null
: coerceComponent(currComponent, defaultWrapper);
});

View 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);
}
);
});

View file

@ -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({