Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions frontend/__tests__/utils/strings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ describe("string utils", () => {
);
});

describe("getWordDirection", () => {
describe("isWordRightToLeft", () => {
beforeEach(() => {
Strings.clearWordDirectionCache();
});
Expand Down Expand Up @@ -321,13 +321,27 @@ describe("string utils", () => {
languageRTL: boolean,
_description: string
) => {
expect(Strings.getWordDirection(word, languageRTL)).toBe(expected);
expect(Strings.isWordRightToLeft(word, languageRTL)).toBe(expected);
}
);

it("should return languageRTL for undefined word", () => {
expect(Strings.getWordDirection(undefined, false)).toBe(false);
expect(Strings.getWordDirection(undefined, true)).toBe(true);
expect(Strings.isWordRightToLeft(undefined, false)).toBe(false);
expect(Strings.isWordRightToLeft(undefined, true)).toBe(true);
});

// testing reverseDirection
it("should return true for LTR word with reversed direction", () => {
expect(Strings.isWordRightToLeft("hello", false, true)).toBe(true);
expect(Strings.isWordRightToLeft("hello", true, true)).toBe(true);
});
it("should return false for RTL word with reversed direction", () => {
expect(Strings.isWordRightToLeft("مرحبا", true, true)).toBe(false);
expect(Strings.isWordRightToLeft("مرحبا", false, true)).toBe(false);
});
it("should return reverse of languageRTL for undefined word with reversed direction", () => {
expect(Strings.isWordRightToLeft(undefined, false, true)).toBe(true);
expect(Strings.isWordRightToLeft(undefined, true, true)).toBe(false);
});

describe("caching", () => {
Expand All @@ -349,7 +363,7 @@ describe("string utils", () => {

it("should use cache for repeated calls", () => {
// First call should cache the result (cache miss)
const result1 = Strings.getWordDirection("hello", false);
const result1 = Strings.isWordRightToLeft("hello", false);
expect(result1).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);

Expand All @@ -358,7 +372,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();

// Second call should use cache (cache hit)
const result2 = Strings.getWordDirection("hello", false);
const result2 = Strings.isWordRightToLeft("hello", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
Expand All @@ -367,47 +381,47 @@ describe("string utils", () => {
mapGetSpy.mockClear();
mapSetSpy.mockClear();

const result3 = Strings.getWordDirection("hello", true);
const result3 = Strings.isWordRightToLeft("hello", true);
expect(result3).toBe(false); // Still false because "hello" is LTR regardless of language
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
});

it("should cache based on core word without punctuation", () => {
// First call should cache the result for core "hello"
const result1 = Strings.getWordDirection("hello", false);
const result1 = Strings.isWordRightToLeft("hello", false);
expect(result1).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);

mapGetSpy.mockClear();
mapSetSpy.mockClear();

// These should all use the same cache entry since they have the same core
const result2 = Strings.getWordDirection("hello!", false);
const result2 = Strings.isWordRightToLeft("hello!", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();

mapGetSpy.mockClear();
mapSetSpy.mockClear();

const result3 = Strings.getWordDirection("!hello", false);
const result3 = Strings.isWordRightToLeft("!hello", false);
expect(result3).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();

mapGetSpy.mockClear();
mapSetSpy.mockClear();

const result4 = Strings.getWordDirection("!hello!", false);
const result4 = Strings.isWordRightToLeft("!hello!", false);
expect(result4).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
});

it("should handle cache clearing", () => {
// Cache a result
Strings.getWordDirection("test", false);
Strings.isWordRightToLeft("test", false);
expect(mapSetSpy).toHaveBeenCalledWith("test", false);

// Clear cache
Expand All @@ -419,14 +433,14 @@ describe("string utils", () => {
mapClearSpy.mockClear();

// Should work normally after cache clear (cache miss again)
const result = Strings.getWordDirection("test", false);
const result = Strings.isWordRightToLeft("test", false);
expect(result).toBe(false);
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
});

it("should demonstrate cache miss vs cache hit behavior", () => {
// Test cache miss - first time seeing this word
const result1 = Strings.getWordDirection("unique", false);
const result1 = Strings.isWordRightToLeft("unique", false);
expect(result1).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
expect(mapSetSpy).toHaveBeenCalledWith("unique", false);
Expand All @@ -435,7 +449,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();

// Test cache hit - same word again
const result2 = Strings.getWordDirection("unique", false);
const result2 = Strings.isWordRightToLeft("unique", false);
expect(result2).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
expect(mapSetSpy).not.toHaveBeenCalled(); // No cache set on hit
Expand All @@ -444,7 +458,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();

// Test cache miss - different word
const result3 = Strings.getWordDirection("different", false);
const result3 = Strings.isWordRightToLeft("different", false);
expect(result3).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("different");
expect(mapSetSpy).toHaveBeenCalledWith("different", false);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/html/popups.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
<div class="title">sentry</div>
<div class="description">
We use Sentry to track errors and performance issues on our site, as
well as record annonymized user sessions to help us debug issues and
well as record anonymized user sessions to help us debug issues and
improve our product.
</div>
<input type="checkbox" />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/privacy-policy.html
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ <h1 id="Sentry">Sentry</h1>
Sentry is a crash reporting service that helps us track errors and
crashes on the website. It collects information about your device,
browser, and the error that occurred. Sometimes it might also include
an annonymized replay of your session. This information is used to
an anonymized replay of your session. This information is used to
track down bugs faster and improve our website.
</p>
<p>
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,6 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
.word {
//flex-direction: row-reverse;
direction: rtl;
}
}
&.withLigatures {
.word {
Expand Down Expand Up @@ -749,10 +745,6 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
.word {
//flex-direction: row-reverse;
direction: rtl;
}
}
&.withLigatures {
.word {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/ts/controllers/input-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,6 @@ async function handleSpace(): Promise<void> {
void TestLogic.addWord();
}
TestUI.updateActiveElement();
void Caret.updatePosition();

const shouldLimitToThreeLines =
Config.mode === "time" ||
Expand All @@ -344,8 +343,10 @@ async function handleSpace(): Promise<void> {

if ((nextTop ?? 0) > currentTop) {
void TestUI.lineJump(currentTop);
} //end of line wrap
}
}
} //end of line wrap

void Caret.updatePosition();

// enable if i decide that auto tab should also work after a space
// if (
Expand Down
12 changes: 3 additions & 9 deletions frontend/src/ts/elements/result-word-highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
// Constants for padding around the highlights

import * as Misc from "../utils/misc";
import * as JSONData from "../utils/json-data";
import Config from "../config";
import * as TestState from "../test/test-state";

const PADDING_X = 16;
const PADDING_Y = 12;
Expand Down Expand Up @@ -56,7 +55,6 @@ let isInitialized = false;
let isHoveringChart = false;
let isFirstHighlightSinceInit = true;
let isFirstHighlightSinceClear = true;
let isLanguageRightToLeft = false;
let isInitInProgress = false;

// Highlights .word elements in range [firstWordIndex, lastWordIndex]
Expand Down Expand Up @@ -104,7 +102,7 @@ export async function highlightWordsInRange(
const newHighlightElementPositions = getHighlightElementPositions(
firstWordIndex,
lastWordIndex,
isLanguageRightToLeft
TestState.isLanguageRightToLeft
);

// For each line...
Expand Down Expand Up @@ -198,10 +196,6 @@ async function init(): Promise<boolean> {
);
}

// Set isLanguageRTL
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;

RWH_el = $("#resultWordsHistory")[0] as HTMLElement;
RWH_rect = RWH_el.getBoundingClientRect();
wordEls = $(RWH_el).find(".words .word[input]");
Expand Down Expand Up @@ -309,7 +303,7 @@ async function init(): Promise<boolean> {

// For RTL languages, account for difference between highlightContainer left and RWH_el left
let RTL_offset;
if (isLanguageRightToLeft) {
if (TestState.isLanguageRightToLeft) {
RTL_offset = line.rect.left - RWH_rect.left + PADDING_X;
} else {
RTL_offset = 0;
Expand Down
24 changes: 8 additions & 16 deletions frontend/src/ts/test/caret.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as JSONData from "../utils/json-data";
import Config from "../config";
import * as TestInput from "./test-input";
import * as SlowTimer from "../states/slow-timer";
import * as TestState from "../test/test-state";
import * as TestWords from "./test-words";
import { convertRemToPixels } from "../utils/numbers";
import { splitIntoCharacters, getWordDirection } from "../utils/strings";
import { splitIntoCharacters, isWordRightToLeft } from "../utils/strings";
import { safeNumber } from "@monkeytype/util/numbers";
import { subscribe } from "../observables/config-event";

Expand Down Expand Up @@ -53,7 +52,6 @@ function getSpaceWidth(wordElement?: HTMLElement): number {

function getTargetPositionLeft(
fullWidthCaret: boolean,
isLanguageRightToLeft: boolean,
activeWordElement: HTMLElement,
currentWordNodeList: NodeListOf<HTMLElement>,
fullWidthCaretWidth: number,
Expand All @@ -65,9 +63,10 @@ function getTargetPositionLeft(
let result = 0;

// use word-specific direction if available and different from language direction
const isWordRightToLeft = getWordDirection(
const isWordRTL = isWordRightToLeft(
currentWord,
isLanguageRightToLeft
TestState.isLanguageRightToLeft,
TestState.isDirectionReversed
);

if (Config.tapeMode === "off") {
Expand All @@ -77,7 +76,7 @@ function getTargetPositionLeft(
const lastWordLetter = currentWordNodeList[wordLen - 1];
const lastInputLetter = currentWordNodeList[inputLen - 1];

if (isWordRightToLeft) {
if (isWordRTL) {
if (inputLen <= wordLen && currentLetter) {
// at word beginning in zen mode both lengths are 0, but currentLetter is defined "_"
positionOffsetToWord =
Expand Down Expand Up @@ -110,13 +109,10 @@ function getTargetPositionLeft(
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
const tapeMargin =
wordsWrapperWidth *
(isWordRightToLeft
? 1 - Config.tapeMargin / 100
: Config.tapeMargin / 100);
(isWordRTL ? 1 - Config.tapeMargin / 100 : Config.tapeMargin / 100);

result =
tapeMargin -
(fullWidthCaret && isWordRightToLeft ? fullWidthCaretWidth : 0);
tapeMargin - (fullWidthCaret && isWordRTL ? fullWidthCaretWidth : 0);

if (Config.tapeMode === "word" && inputLen > 0) {
let currentWordWidth = 0;
Expand All @@ -131,7 +127,7 @@ function getTargetPositionLeft(
// if current letter has zero width move the caret to previous positive width letter
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
currentWordWidth -= lastPositiveLetterWidth;
if (isWordRightToLeft) currentWordWidth *= -1;
if (isWordRTL) currentWordWidth *= -1;
result += currentWordWidth;
}
}
Expand Down Expand Up @@ -185,9 +181,6 @@ export async function updatePosition(noAnim = false): Promise<void> {
const lastInputLetter = currentWordNodeList[inputLen - 1];
const lastWordLetter = currentWordNodeList[wordLen - 1];

const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
const isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;

// in blind mode, and hide extra letters, extra letters have zero offsets
// offsetHeight is the same for all visible letters
// so is offsetTop (for same line letters)
Expand Down Expand Up @@ -224,7 +217,6 @@ export async function updatePosition(noAnim = false): Promise<void> {

const letterPosLeft = getTargetPositionLeft(
fullWidthCaret,
isLanguageRightToLeft,
activeWordEl,
currentWordNodeList,
letterWidth,
Expand Down
Loading
Loading