Repository: chanind/hanzi-writer
Pull request #317 (2024-05-21 16:56:34)

feat: Added skipStrokeQuiz
This feature adds a function skipStrokeQuiz, that can be used while in a quiz to skip a stroke. This is useful for e.g. 圖, since hanzi writer has a hard time recognizing the short strike under the upper inner box of
![clipboard](https://github.com/chanind/hanzi-writer/assets/50072577/a34019a2-44d2-412a-ac65-d0cfeab8f50e)




codecov-commenter: ## [Codecov](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin) Report
Attention: Patch coverage is `66.66667%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 96.25%. Comparing base [(`4ec4eda`)](https://app.codecov.io/gh/chanind/hanzi-writer/commit/4ec4edaeeeb5caef7e193068cc5e05ee6fe08ac8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin) to head [(`6803094`)](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin).

| [Files](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin) | Patch % | Lines |
|---|---|---|
| [src/HanziWriter.ts](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?src=pr&el=tree&filepath=src%2FHanziWriter.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin#diff-c3JjL0hhbnppV3JpdGVyLnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #317      +/-   ##
==========================================
- Coverage   96.41%   96.25%   -0.16%     
==========================================
  Files          32       32              
  Lines        1144     1149       +5     
  Branches      212      214       +2     
==========================================
+ Hits         1103     1106       +3     
- Misses         37       39       +2     
  Partials        4        4              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/chanind/hanzi-writer/pull/317?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=David+Chanin).

chanind: Thanks for contributing this! I tweaked the API slightly so it's called `skipQuizStroke`, and added test coverage.

Patch:
diff --git a/src/HanziWriter.ts b/src/HanziWriter.ts
index 60d20ecb..1730164c 100644
--- a/src/HanziWriter.ts
+++ b/src/HanziWriter.ts
@@ -405,6 +405,12 @@ export default class HanziWriter {
     });
   }
 
+  skipQuizStroke() {
+    if (this._quiz) {
+      this._quiz.nextStroke();
+    }
+  }
+
   cancelQuiz() {
     if (this._quiz) {
       this._quiz.cancel();
diff --git a/src/Quiz.ts b/src/Quiz.ts
index 2c1ba36f..61695b06 100644
--- a/src/Quiz.ts
+++ b/src/Quiz.ts
@@ -127,8 +127,11 @@ export default class Quiz {
     } else {
       this._handleFailure(meta);
 
-      const { showHintAfterMisses, highlightColor, strokeHighlightSpeed } =
-        this._options!;
+      const {
+        showHintAfterMisses,
+        highlightColor,
+        strokeHighlightSpeed,
+      } = this._options!;
 
       if (
         showHintAfterMisses !== false &&
@@ -178,13 +181,12 @@ export default class Quiz {
     };
   }
 
-  _handleSuccess(meta: StrokeMatchResultMeta) {
+  nextStroke() {
     if (!this._options) return;
 
     const { strokes, symbol } = this._character;
 
     const {
-      onCorrectStroke,
       onComplete,
       highlightOnComplete,
       strokeFadeDuration,
@@ -193,10 +195,6 @@ export default class Quiz {
       strokeHighlightDuration,
     } = this._options;
 
-    onCorrectStroke?.({
-      ...this._getStrokeData({ isCorrect: true, meta }),
-    });
-
     let animation: GenericMutation[] = characterActions.showStroke(
       'main',
       this._currentStrokeIndex,
@@ -228,6 +226,18 @@ export default class Quiz {
     this._renderState.run(animation);
   }
 
+  _handleSuccess(meta: StrokeMatchResultMeta) {
+    if (!this._options) return;
+
+    const { onCorrectStroke } = this._options;
+
+    onCorrectStroke?.({
+      ...this._getStrokeData({ isCorrect: true, meta }),
+    });
+
+    this.nextStroke();
+  }
+
   _handleFailure(meta: StrokeMatchResultMeta) {
     this._mistakesOnStroke += 1;
     this._totalMistakes += 1;


Test patch:
diff --git a/src/__tests__/HanziWriter-test.ts b/src/__tests__/HanziWriter-test.ts
index 37e6a25e..62b5db30 100644
--- a/src/__tests__/HanziWriter-test.ts
+++ b/src/__tests__/HanziWriter-test.ts
@@ -1140,6 +1140,27 @@ describe('HanziWriter', () => {
     });
   });
 
+  describe('skipQuizStroke', () => {
+    it('returns if there is not active quiz', async () => {
+      document.body.innerHTML = '<div id="target"></div>';
+      const writer = HanziWriter.create('target', '人', { charDataLoader });
+      await writer._withDataPromise;
+      expect(writer.skipQuizStroke()).toBe(undefined);
+      expect(writer._quiz).toBe(undefined);
+    });
+
+    it('skips the current stroke if a quiz is active', async () => {
+      document.body.innerHTML = '<div id="target"></div>';
+      const writer = HanziWriter.create('target', '人', { charDataLoader });
+      writer.quiz();
+      await resolvePromises();
+      const quiz = writer._quiz!;
+      writer.skipQuizStroke();
+      expect(quiz.nextStroke).toHaveBeenCalledTimes(1);
+      expect(quiz._handleSuccess).not.toHaveBeenCalled();
+    });
+  });
+
   describe('mouse and touch events', () => {
     let writer: HanziWriter;
     beforeEach(async () => {
diff --git a/src/__tests__/Quiz-test.ts b/src/__tests__/Quiz-test.ts
index 5b662e75..d2723e6d 100644
--- a/src/__tests__/Quiz-test.ts
+++ b/src/__tests__/Quiz-test.ts
@@ -956,6 +956,40 @@ describe('Quiz', () => {
     });
   });
 
+  describe('nextStroke', () => {
+    it('shows the current stroke and moves to the next stroke', async () => {
+      const renderState = createRenderState();
+      const quiz = new Quiz(
+        char,
+        renderState,
+        new Positioner({ padding: 20, width: 200, height: 200 }),
+      );
+      const onCorrectStroke = jest.fn();
+      const onMistake = jest.fn();
+      const onComplete = jest.fn();
+      quiz.startQuiz(Object.assign({}, opts, { onCorrectStroke, onComplete, onMistake }));
+      clock.tick(1000);
+      await resolvePromises();
+
+      expect(quiz._currentStrokeIndex).toBe(0);
+      quiz.nextStroke();
+      await resolvePromises();
+
+      expect(quiz._userStroke).toBeUndefined();
+      expect(quiz._isActive).toBe(true);
+      expect(quiz._currentStrokeIndex).toBe(1);
+      expect(onCorrectStroke).not.toHaveBeenCalled();
+      expect(onMistake).not.toHaveBeenCalled();
+      expect(onComplete).not.toHaveBeenCalled();
+
+      clock.tick(1000);
+      await resolvePromises();
+
+      expect(renderState.state.character.main.strokes[0].opacity).toBe(1);
+      expect(renderState.state.character.main.strokes[1].opacity).toBe(0);
+    });
+  });
+
   it('doesnt leave strokes partially drawn if the users finishes the quiz really fast', async () => {
     (strokeMatches as any).mockImplementation(() => ({
       isMatch: true,