diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/data/anki-note-builder-test-results.json | 48 | ||||
-rw-r--r-- | test/data/html/test-document2-script.js | 40 | ||||
-rw-r--r-- | test/data/translator-test-results-note-data1.json | 280 | ||||
-rw-r--r-- | test/data/translator-test-results.json | 236 | ||||
-rw-r--r-- | test/dictionary.test.js | 6 | ||||
-rw-r--r-- | test/jsconfig.json | 39 | ||||
-rw-r--r-- | test/playwright/visual.spec.js | 12 | ||||
-rw-r--r-- | test/test-all.js | 71 | ||||
-rw-r--r-- | test/test-anki-note-builder.js | 322 | ||||
-rw-r--r-- | test/test-cache-map.js | 137 | ||||
-rw-r--r-- | test/test-core.js | 300 | ||||
-rw-r--r-- | test/test-database.js | 982 | ||||
-rw-r--r-- | test/test-document-util.js | 339 | ||||
-rw-r--r-- | test/test-hotkey-util.js | 189 | ||||
-rw-r--r-- | test/test-japanese-util.js | 915 | ||||
-rw-r--r-- | test/test-json-schema.js | 1048 | ||||
-rw-r--r-- | test/test-manifest.js | 49 | ||||
-rw-r--r-- | test/test-object-property-accessor.js | 458 | ||||
-rw-r--r-- | test/test-profile-conditions-util.js | 1136 | ||||
-rw-r--r-- | test/test-text-source-map.js | 244 | ||||
-rw-r--r-- | test/test-translator.js | 102 | ||||
-rw-r--r-- | test/test-workers.js | 168 |
22 files changed, 6833 insertions, 288 deletions
diff --git a/test/data/anki-note-builder-test-results.json b/test/data/anki-note-builder-test-results.json index b752e878..49542e39 100644 --- a/test/data/anki-note-builder-test-results.json +++ b/test/data/anki-note-builder-test-results.json @@ -194,7 +194,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -224,7 +224,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -379,7 +379,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[う]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>", @@ -409,7 +409,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[ぶ]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>", @@ -499,7 +499,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -529,7 +529,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -759,7 +759,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -824,7 +824,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -889,7 +889,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[う]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>", @@ -949,7 +949,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -1014,7 +1014,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[ぶ]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>", @@ -1074,7 +1074,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -1526,7 +1526,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[う]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>", @@ -1556,7 +1556,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[ぶ]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>", @@ -1646,7 +1646,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -1676,7 +1676,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -1831,7 +1831,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[う]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>", @@ -1861,7 +1861,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[ぶ]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>", @@ -1951,7 +1951,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -1981,7 +1981,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", @@ -2136,7 +2136,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[う]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>", @@ -2166,7 +2166,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打ち込む", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む", "furigana-plain": "打[ぶ]ち 込[こ]む", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>", @@ -2256,7 +2256,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>", "furigana": "<ruby>打<rt>う</rt></ruby>つ", "furigana-plain": "打[う]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>", @@ -2286,7 +2286,7 @@ "dictionary": "Test Dictionary 2", "document-title": "title", "expression": "打つ", - "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>", + "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>", "furigana": "<ruby>打<rt>ぶ</rt></ruby>つ", "furigana-plain": "打[ぶ]つ", "glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>", diff --git a/test/data/html/test-document2-script.js b/test/data/html/test-document2-script.js index 8a183019..5a6ad4d1 100644 --- a/test/data/html/test-document2-script.js +++ b/test/data/html/test-document2-script.js @@ -16,40 +16,65 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +/** + * @param {Element} element + */ function requestFullscreen(element) { if (element.requestFullscreen) { element.requestFullscreen(); + // @ts-ignore - Browser compatibility } else if (element.mozRequestFullScreen) { + // @ts-ignore - Browser compatibility element.mozRequestFullScreen(); + // @ts-ignore - Browser compatibility } else if (element.webkitRequestFullscreen) { + // @ts-ignore - Browser compatibility element.webkitRequestFullscreen(); + // @ts-ignore - Browser compatibility } else if (element.msRequestFullscreen) { + // @ts-ignore - Browser compatibility element.msRequestFullscreen(); } } +/** */ function exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); + // @ts-ignore - Browser compatibility } else if (document.mozCancelFullScreen) { + // @ts-ignore - Browser compatibility document.mozCancelFullScreen(); + // @ts-ignore - Browser compatibility } else if (document.webkitExitFullscreen) { + // @ts-ignore - Browser compatibility document.webkitExitFullscreen(); + // @ts-ignore - Browser compatibility } else if (document.msExitFullscreen) { + // @ts-ignore - Browser compatibility document.msExitFullscreen(); } } +/** + * @returns {?Element} + */ function getFullscreenElement() { return ( document.fullscreenElement || + // @ts-ignore - Browser compatibility document.msFullscreenElement || + // @ts-ignore - Browser compatibility document.mozFullScreenElement || + // @ts-ignore - Browser compatibility document.webkitFullscreenElement || null ); } +/** + * @param {Element} element + */ function toggleFullscreen(element) { if (getFullscreenElement()) { exitFullscreen(); @@ -58,6 +83,10 @@ function toggleFullscreen(element) { } } +/** + * @param {HTMLElement|DocumentFragment} container + * @param {?Element} [fullscreenElement] + */ function setup(container, fullscreenElement=null) { const fullscreenLink = container.querySelector('.fullscreen-link'); if (fullscreenLink !== null) { @@ -65,6 +94,7 @@ function setup(container, fullscreenElement=null) { fullscreenElement = container.querySelector('.fullscreen-element'); } fullscreenLink.addEventListener('click', (e) => { + if (fullscreenElement === null) { return; } toggleFullscreen(fullscreenElement); e.preventDefault(); return false; @@ -74,11 +104,15 @@ function setup(container, fullscreenElement=null) { const template = container.querySelector('template'); const templateContentContainer = container.querySelector('.template-content-container'); if (template !== null && templateContentContainer !== null) { - const mode = container.dataset.shadowMode; - const shadow = templateContentContainer.attachShadow({mode}); + const mode = (container instanceof HTMLElement ? container.dataset.shadowMode : void 0); + const shadow = templateContentContainer.attachShadow({ + mode: (mode === 'open' || mode === 'closed' ? mode : 'open') + }); const containerStyles = document.querySelector('#container-styles'); - shadow.appendChild(containerStyles.cloneNode(true)); + if (containerStyles !== null) { + shadow.appendChild(containerStyles.cloneNode(true)); + } const content = document.importNode(template.content, true); setup(content); diff --git a/test/data/translator-test-results-note-data1.json b/test/data/translator-test-results-note-data1.json index d686563c..34f7c21a 100644 --- a/test/data/translator-test-results-note-data1.json +++ b/test/data/translator-test-results-note-data1.json @@ -1659,7 +1659,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -1672,7 +1672,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -1791,7 +1791,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -1804,7 +1804,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -1963,7 +1963,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -1976,7 +1976,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -1989,7 +1989,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -2095,7 +2095,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -2108,7 +2108,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -2121,7 +2121,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -3651,7 +3651,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -3677,7 +3677,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -3690,7 +3690,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -3813,7 +3813,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -3839,7 +3839,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -3852,7 +3852,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -4045,7 +4045,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -4071,7 +4071,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -4084,7 +4084,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -4097,7 +4097,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -4207,7 +4207,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -4233,7 +4233,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -4246,7 +4246,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -4259,7 +4259,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -5079,7 +5079,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -5092,7 +5092,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -5211,7 +5211,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -5224,7 +5224,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -5385,7 +5385,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -5398,7 +5398,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -5411,7 +5411,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -5517,7 +5517,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -5530,7 +5530,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -5543,7 +5543,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -7394,7 +7394,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -7407,7 +7407,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -7526,7 +7526,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -7539,7 +7539,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -8007,7 +8007,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -8020,7 +8020,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -8033,7 +8033,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -8139,7 +8139,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -8152,7 +8152,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -8165,7 +8165,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -8684,7 +8684,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -8710,7 +8710,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -8723,7 +8723,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -8846,7 +8846,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -8872,7 +8872,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -8885,7 +8885,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -9412,7 +9412,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -9425,7 +9425,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -9544,7 +9544,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -9557,7 +9557,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -10089,7 +10089,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -10115,7 +10115,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -10128,7 +10128,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -10141,7 +10141,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -10251,7 +10251,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -10277,7 +10277,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -10290,7 +10290,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -10303,7 +10303,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -10817,7 +10817,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -10830,7 +10830,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -10843,7 +10843,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -10949,7 +10949,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -10962,7 +10962,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -10975,7 +10975,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -16097,7 +16097,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -16123,7 +16123,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -16136,7 +16136,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -16259,7 +16259,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -16285,7 +16285,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -16298,7 +16298,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -16495,7 +16495,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -16521,7 +16521,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -16534,7 +16534,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -16547,7 +16547,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -16657,7 +16657,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -16683,7 +16683,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -16696,7 +16696,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -16709,7 +16709,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -17529,7 +17529,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -17542,7 +17542,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -17661,7 +17661,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -17674,7 +17674,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -17835,7 +17835,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -17848,7 +17848,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -17861,7 +17861,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -17967,7 +17967,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -17980,7 +17980,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -17993,7 +17993,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -19523,7 +19523,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -19549,7 +19549,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -19562,7 +19562,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -19685,7 +19685,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -19711,7 +19711,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -19724,7 +19724,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -19917,7 +19917,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -19943,7 +19943,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -19956,7 +19956,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -19969,7 +19969,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -20079,7 +20079,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -20105,7 +20105,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -20118,7 +20118,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -20131,7 +20131,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -20951,7 +20951,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -20964,7 +20964,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -21083,7 +21083,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -21096,7 +21096,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -21257,7 +21257,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -21270,7 +21270,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -21283,7 +21283,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -21389,7 +21389,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -21402,7 +21402,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -21415,7 +21415,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -22945,7 +22945,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -22971,7 +22971,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -22984,7 +22984,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -23107,7 +23107,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -23133,7 +23133,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 0 + "frequency": "eighteen" }, { "index": 4, @@ -23146,7 +23146,7 @@ "expression": "打ち込む", "reading": "うちこむ", "hasReading": true, - "frequency": 24 + "frequency": "twenty-four (24)" }, { "index": 5, @@ -23339,7 +23339,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -23365,7 +23365,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -23378,7 +23378,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -23391,7 +23391,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -23501,7 +23501,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": false, - "frequency": 7 + "frequency": "seven" }, { "index": 2, @@ -23527,7 +23527,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 0 + "frequency": "nineteen" }, { "index": 4, @@ -23540,7 +23540,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 25 + "frequency": "twenty-five (25)" }, { "index": 5, @@ -23553,7 +23553,7 @@ "expression": "打ち込む", "reading": "ぶちこむ", "hasReading": true, - "frequency": 31 + "frequency": "thirty-one" } ], "pitches": [ @@ -24373,7 +24373,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -24386,7 +24386,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -24505,7 +24505,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 0 + "frequency": "sixteen" }, { "index": 4, @@ -24518,7 +24518,7 @@ "expression": "打つ", "reading": "うつ", "hasReading": true, - "frequency": 22 + "frequency": "twenty-two (22)" }, { "index": 5, @@ -24679,7 +24679,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -24692,7 +24692,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -24705,7 +24705,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], @@ -24811,7 +24811,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 0 + "frequency": "seventeen" }, { "index": 4, @@ -24824,7 +24824,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 23 + "frequency": "twenty-three (23)" }, { "index": 5, @@ -24837,7 +24837,7 @@ "expression": "打つ", "reading": "ぶつ", "hasReading": true, - "frequency": 29 + "frequency": "twenty-nine" } ], "pitches": [], diff --git a/test/data/translator-test-results.json b/test/data/translator-test-results.json index 98db0ef4..0a7155b8 100644 --- a/test/data/translator-test-results.json +++ b/test/data/translator-test-results.json @@ -1101,8 +1101,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -1112,8 +1112,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -1266,8 +1266,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -1277,8 +1277,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -1288,7 +1288,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -2150,7 +2150,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -2172,8 +2172,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "eighteen", + "displayValueParsed": true }, { "index": 4, @@ -2183,8 +2183,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 24, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-four (24)", + "displayValueParsed": true }, { "index": 5, @@ -2337,7 +2337,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -2359,8 +2359,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "nineteen", + "displayValueParsed": true }, { "index": 4, @@ -2370,8 +2370,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 25, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-five (25)", + "displayValueParsed": true }, { "index": 5, @@ -2381,7 +2381,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 31, - "displayValue": null, + "displayValue": "thirty-one", "displayValueParsed": false } ] @@ -2860,8 +2860,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -2871,8 +2871,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -3027,8 +3027,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -3038,8 +3038,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -3049,7 +3049,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -4166,8 +4166,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -4177,8 +4177,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -4502,8 +4502,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -4513,8 +4513,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -4524,7 +4524,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -4860,7 +4860,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -4882,8 +4882,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "eighteen", + "displayValueParsed": true }, { "index": 4, @@ -4893,8 +4893,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 24, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-four (24)", + "displayValueParsed": true }, { "index": 5, @@ -5216,8 +5216,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -5227,8 +5227,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -5574,7 +5574,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -5596,8 +5596,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "nineteen", + "displayValueParsed": true }, { "index": 4, @@ -5607,8 +5607,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 25, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-five (25)", + "displayValueParsed": true }, { "index": 5, @@ -5618,7 +5618,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 31, - "displayValue": null, + "displayValue": "thirty-one", "displayValueParsed": false } ] @@ -5930,8 +5930,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -5941,8 +5941,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -5952,7 +5952,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -9645,7 +9645,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -9667,8 +9667,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "eighteen", + "displayValueParsed": true }, { "index": 4, @@ -9678,8 +9678,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 24, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-four (24)", + "displayValueParsed": true }, { "index": 5, @@ -9836,7 +9836,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -9858,8 +9858,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "nineteen", + "displayValueParsed": true }, { "index": 4, @@ -9869,8 +9869,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 25, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-five (25)", + "displayValueParsed": true }, { "index": 5, @@ -9880,7 +9880,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 31, - "displayValue": null, + "displayValue": "thirty-one", "displayValueParsed": false } ] @@ -10359,8 +10359,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -10370,8 +10370,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -10526,8 +10526,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -10537,8 +10537,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -10548,7 +10548,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -11410,7 +11410,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -11432,8 +11432,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "eighteen", + "displayValueParsed": true }, { "index": 4, @@ -11443,8 +11443,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 24, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-four (24)", + "displayValueParsed": true }, { "index": 5, @@ -11597,7 +11597,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -11619,8 +11619,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "nineteen", + "displayValueParsed": true }, { "index": 4, @@ -11630,8 +11630,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 25, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-five (25)", + "displayValueParsed": true }, { "index": 5, @@ -11641,7 +11641,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 31, - "displayValue": null, + "displayValue": "thirty-one", "displayValueParsed": false } ] @@ -12120,8 +12120,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -12131,8 +12131,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -12287,8 +12287,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -12298,8 +12298,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -12309,7 +12309,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] @@ -13171,7 +13171,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -13193,8 +13193,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "eighteen", + "displayValueParsed": true }, { "index": 4, @@ -13204,8 +13204,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 24, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-four (24)", + "displayValueParsed": true }, { "index": 5, @@ -13358,7 +13358,7 @@ "dictionaryPriority": 0, "hasReading": false, "frequency": 7, - "displayValue": null, + "displayValue": "seven", "displayValueParsed": false }, { @@ -13380,8 +13380,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "nineteen", + "displayValueParsed": true }, { "index": 4, @@ -13391,8 +13391,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 25, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-five (25)", + "displayValueParsed": true }, { "index": 5, @@ -13402,7 +13402,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 31, - "displayValue": null, + "displayValue": "thirty-one", "displayValueParsed": false } ] @@ -13881,8 +13881,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "sixteen", + "displayValueParsed": true }, { "index": 4, @@ -13892,8 +13892,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 22, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-two (22)", + "displayValueParsed": true }, { "index": 5, @@ -14048,8 +14048,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 0, - "displayValue": null, - "displayValueParsed": false + "displayValue": "seventeen", + "displayValueParsed": true }, { "index": 4, @@ -14059,8 +14059,8 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 23, - "displayValue": null, - "displayValueParsed": false + "displayValue": "twenty-three (23)", + "displayValueParsed": true }, { "index": 5, @@ -14070,7 +14070,7 @@ "dictionaryPriority": 0, "hasReading": true, "frequency": 29, - "displayValue": null, + "displayValue": "twenty-nine", "displayValueParsed": false } ] diff --git a/test/dictionary.test.js b/test/dictionary.test.js index 8f160bc1..e516bd8e 100644 --- a/test/dictionary.test.js +++ b/test/dictionary.test.js @@ -24,12 +24,18 @@ import {createDictionaryArchive} from '../dev/util.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * @param {string} dictionary + * @param {string} [dictionaryName] + * @returns {import('jszip')} + */ function createTestDictionaryArchive(dictionary, dictionaryName) { const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', dictionary); return createDictionaryArchive(dictionaryDirectory, dictionaryName); } +/** */ async function main() { const dictionaries = [ {name: 'valid-dictionary1', valid: true}, diff --git a/test/jsconfig.json b/test/jsconfig.json new file mode 100644 index 00000000..b025918c --- /dev/null +++ b/test/jsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "module": "ES2015", + "target": "ES2022", + "checkJs": true, + "moduleResolution": "node", + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictPropertyInitialization": true, + "suppressImplicitAnyIndexErrors": false, + "skipLibCheck": false, + "baseUrl": ".", + "paths": { + "*": ["../types/ext/*"], + "dev/*": ["../types/dev/*"], + "test/*": ["../types/test/*"] + }, + "types": [ + "chrome", + "firefox-webext-browser", + "handlebars", + "jszip", + "parse5", + "wanakana" + ] + }, + "include": [ + "**/*.js", + "../ext/**/*.js", + "../types/ext/**/*.ts", + "../types/dev/**/*.ts", + "../types/other/web-set-timeout.d.ts" + ], + "exclude": [ + "../node_modules", + "../ext/lib" + ] +}
\ No newline at end of file diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js index 2f46990f..8b48b7c0 100644 --- a/test/playwright/visual.spec.js +++ b/test/playwright/visual.spec.js @@ -48,10 +48,16 @@ test('visual', async ({page, extensionId}) => { // take a screenshot of the settings page with jmdict loaded await expect.soft(page).toHaveScreenshot('settings-jmdict-loaded.png', {mask: [storage_locator]}); + /** + * @param {number} doc_number + * @param {number} test_number + * @param {import('@playwright/test').ElementHandle<Node>} el + * @param {{x: number, y: number}} offset + */ const screenshot = async (doc_number, test_number, el, offset) => { const test_name = 'doc' + doc_number + '-test' + test_number; - const box = await el.boundingBox(); + const box = (await el.boundingBox()) || {x: 0, y: 0, width: 0, height: 0}; // find the popup frame if it exists let popup_frame = page.frames().find((f) => f.url().includes('popup.html')); @@ -66,7 +72,7 @@ test('visual', async ({page, extensionId}) => { popup_frame = await frame_attached; // wait for popup to be attached } try { - await (await popup_frame.frameElement()).waitForElementState('visible', {timeout: 500}); // some tests don't have a popup, so don't fail if it's not there; TODO: check if the popup is expected to be there + await (await /** @type {import('@playwright/test').Frame} */ (popup_frame).frameElement()).waitForElementState('visible', {timeout: 500}); // some tests don't have a popup, so don't fail if it's not there; TODO: check if the popup is expected to be there } catch (error) { console.log(test_name + ' has no popup'); } @@ -75,7 +81,7 @@ test('visual', async ({page, extensionId}) => { await expect.soft(page).toHaveScreenshot(test_name + '.png'); await page.mouse.click(0, 0); // click away so popup disappears - await (await popup_frame.frameElement()).waitForElementState('hidden'); // wait for popup to disappear + await (await /** @type {import('@playwright/test').Frame} */ (popup_frame).frameElement()).waitForElementState('hidden'); // wait for popup to disappear }; // Load test-document1.html diff --git a/test/test-all.js b/test/test-all.js new file mode 100644 index 00000000..9219d278 --- /dev/null +++ b/test/test-all.js @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const {spawnSync} = require('child_process'); +const {getArgs} = require('../dev/util'); + + +/** + * @throws {Error} + */ +function main() { + const args = getArgs(process.argv.slice(2), new Map(/** @type {[key: string, value: (boolean|null|number|string|string[])][]} */ ([ + ['skip', []], + [null, []] + ]))); + const directories = /** @type {string[]} */ (args.get(null)); + const skipArg = /** @type {string[]} */ (args.get('skip')); + const skip = new Set([__filename, ...skipArg].map((value) => path.resolve(value))); + + const node = process.execPath; + const fileNamePattern = /\.js$/i; + + let first = true; + for (const directory of directories) { + const fileNames = fs.readdirSync(directory); + for (const fileName of fileNames) { + if (!fileNamePattern.test(fileName)) { continue; } + + const fullFileName = path.resolve(path.join(directory, fileName)); + if (skip.has(fullFileName)) { continue; } + + const stats = fs.lstatSync(fullFileName); + if (!stats.isFile()) { continue; } + + process.stdout.write(`${first ? '' : '\n'}Running ${fileName}...\n`); + first = false; + + const {error, status} = spawnSync(node, [fileName], {cwd: directory, stdio: 'inherit'}); + + if (status !== null && status !== 0) { + process.exit(status); + return; + } + if (error) { + throw error; + } + } + } + + process.exit(0); +} + + +if (require.main === module) { main(); } diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js new file mode 100644 index 00000000..8e0ab9d9 --- /dev/null +++ b/test/test-anki-note-builder.js @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2021-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {JSDOM} = require('jsdom'); +const {testMain} = require('../dev/util'); +const {TranslatorVM} = require('../dev/translator-vm'); + + +/** + * @template T + * @param {T} value + * @returns {T} + */ +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +/** + * @returns {Promise<{vm: TranslatorVM, AnkiNoteBuilder: typeof AnkiNoteBuilder2, JapaneseUtil: typeof JapaneseUtil2}>} + */ +async function createVM() { + const dom = new JSDOM(); + const {Node, NodeFilter, document} = dom.window; + + const vm = new TranslatorVM({ + Node, + NodeFilter, + document, + location: new URL('https://yomichan.test/') + }); + + const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1'); + await vm.prepare(dictionaryDirectory, 'Test Dictionary 2'); + + vm.execute([ + 'js/data/anki-note-builder.js', + 'js/data/anki-util.js', + 'js/dom/sandbox/css-style-applier.js', + 'js/display/sandbox/pronunciation-generator.js', + 'js/display/sandbox/structured-content-generator.js', + 'js/templates/sandbox/anki-template-renderer.js', + 'js/templates/sandbox/anki-template-renderer-content-manager.js', + 'js/templates/sandbox/template-renderer.js', + 'js/templates/sandbox/template-renderer-media-provider.js', + 'lib/handlebars.min.js' + ]); + + /** @type {[typeof JapaneseUtil, typeof AnkiNoteBuilder, typeof AnkiTemplateRenderer]} */ + const [ + JapaneseUtil2, + AnkiNoteBuilder2, + AnkiTemplateRenderer2 + ] = vm.get([ + 'JapaneseUtil', + 'AnkiNoteBuilder', + 'AnkiTemplateRenderer' + ]); + + class TemplateRendererProxy { + constructor() { + /** @type {?Promise<void>} */ + this._preparePromise = null; + /** @type {AnkiTemplateRenderer} */ + this._ankiTemplateRenderer = new AnkiTemplateRenderer2(); + } + + /** + * @param {string} template + * @param {import('template-renderer').PartialOrCompositeRenderData} data + * @param {import('anki-templates').RenderMode} type + * @returns {Promise<import('template-renderer').RenderResult>} + */ + async render(template, data, type) { + await this._prepare(); + return await this._ankiTemplateRenderer.templateRenderer.render(template, data, type); + } + + /** + * @param {import('template-renderer').RenderMultiItem[]} items + * @returns {Promise<import('core').Response<import('template-renderer').RenderResult>[]>} + */ + async renderMulti(items) { + await this._prepare(); + return await this._ankiTemplateRenderer.templateRenderer.renderMulti(items); + } + + /** + * @returns {Promise<void>} + */ + _prepare() { + if (this._preparePromise === null) { + this._preparePromise = this._prepareInternal(); + } + return this._preparePromise; + } + + /** */ + async _prepareInternal() { + await this._ankiTemplateRenderer.prepare(); + } + } + vm.set({TemplateRendererProxy}); + + return {vm, AnkiNoteBuilder: AnkiNoteBuilder2, JapaneseUtil: JapaneseUtil2}; +} + +/** + * @param {'terms'|'kanji'} type + * @returns {string[]} + */ +function getFieldMarkers(type) { + switch (type) { + case 'terms': + return [ + 'audio', + 'clipboard-image', + 'clipboard-text', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'conjugation', + 'dictionary', + 'document-title', + 'expression', + 'frequencies', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'glossary-no-dictionary', + 'part-of-speech', + 'pitch-accents', + 'pitch-accent-graphs', + 'pitch-accent-positions', + 'reading', + 'screenshot', + 'search-query', + 'selection-text', + 'sentence', + 'sentence-furigana', + 'tags', + 'url' + ]; + case 'kanji': + return [ + 'character', + 'clipboard-image', + 'clipboard-text', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'document-title', + 'glossary', + 'kunyomi', + 'onyomi', + 'screenshot', + 'search-query', + 'selection-text', + 'sentence', + 'sentence-furigana', + 'stroke-count', + 'tags', + 'url' + ]; + default: + return []; + } +} + +/** + * @param {import('dictionary').DictionaryEntry[]} dictionaryEntries + * @param {'terms'|'kanji'} type + * @param {import('settings').ResultOutputMode} mode + * @param {string} template + * @param {typeof AnkiNoteBuilder} AnkiNoteBuilder + * @param {typeof JapaneseUtil} JapaneseUtil + * @param {boolean} write + * @returns {Promise<import('anki').NoteFields[]>} + */ +async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNoteBuilder, JapaneseUtil, write) { + const markers = getFieldMarkers(type); + /** @type {import('anki-note-builder').Field[]} */ + const fields = []; + for (const marker of markers) { + fields.push([marker, `{${marker}}`]); + } + + const japaneseUtil = new JapaneseUtil(null); + const clozePrefix = 'cloze-prefix'; + const clozeSuffix = 'cloze-suffix'; + const results = []; + for (const dictionaryEntry of dictionaryEntries) { + let source = ''; + switch (dictionaryEntry.type) { + case 'kanji': + source = dictionaryEntry.character; + break; + case 'term': + if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) { + source = dictionaryEntry.headwords[0].sources[0].originalText; + } + break; + } + const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); + const context = { + url: 'url:', + sentence: { + text: `${clozePrefix}${source}${clozeSuffix}`, + offset: clozePrefix.length + }, + documentTitle: 'title', + query: 'query', + fullQuery: 'fullQuery' + }; + /** @type {import('anki-note-builder').CreateNoteDetails} */ + const details = { + dictionaryEntry, + mode: 'test', + context, + template, + deckName: 'deckName', + modelName: 'modelName', + fields, + tags: ['yomichan'], + checkForDuplicates: true, + duplicateScope: 'collection', + duplicateScopeCheckAllModels: false, + resultOutputMode: mode, + glossaryLayoutMode: 'default', + compactTags: false, + requirements: [], + mediaOptions: null + }; + const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote(details); + if (!write) { + for (const error of errors) { + console.error(error); + } + assert.strictEqual(errors.length, 0); + } + results.push(noteFields); + } + + return results; +} + + +/** */ +async function main() { + const write = (process.argv[2] === '--write'); + + const {vm, AnkiNoteBuilder, JapaneseUtil} = await createVM(); + + const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json'); + const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); + + const testResults1FilePath = path.join(__dirname, 'data', 'anki-note-builder-test-results.json'); + const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); + const actualResults1 = []; + + const template = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); + + for (let i = 0, ii = tests.length; i < ii; ++i) { + const test = tests[i]; + const expected1 = expectedResults1[i]; + switch (test.func) { + case 'findTerms': + { + const {name, mode, text} = test; + /** @type {import('translation').FindTermsOptions} */ + const options = vm.buildOptions(optionsPresets, test.options); + const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options)); + const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, template, AnkiNoteBuilder, JapaneseUtil, write)) : null; + actualResults1.push({name, results}); + if (!write) { + assert.deepStrictEqual(results, expected1.results); + } + } + break; + case 'findKanji': + { + const {name, text} = test; + /** @type {import('translation').FindKanjiOptions} */ + const options = vm.buildOptions(optionsPresets, test.options); + const dictionaryEntries = clone(await vm.translator.findKanji(text, options)); + const results = clone(await getRenderResults(dictionaryEntries, 'kanji', 'split', template, AnkiNoteBuilder, JapaneseUtil, write)); + actualResults1.push({name, results}); + if (!write) { + assert.deepStrictEqual(results, expected1.results); + } + } + break; + } + } + + if (write) { + // Use 2 indent instead of 4 to save a bit of file size + fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'}); + } +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-cache-map.js b/test/test-cache-map.js new file mode 100644 index 00000000..56a31898 --- /dev/null +++ b/test/test-cache-map.js @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM({console}); +vm.execute([ + 'js/general/cache-map.js' +]); +/** @type {typeof CacheMap} */ +const CacheMap2 = vm.getSingle('CacheMap'); + + +/** */ +function testConstructor() { + const data = /** @type {[throws: boolean, create: () => void][]} */ ([ + [false, () => new CacheMap2(0)], + [false, () => new CacheMap2(1)], + [false, () => new CacheMap2(Number.MAX_VALUE)], + [true, () => new CacheMap2(-1)], + [true, () => new CacheMap2(1.5)], + [true, () => new CacheMap2(Number.NaN)], + [true, () => new CacheMap2(Number.POSITIVE_INFINITY)], + // @ts-ignore - Ignore because it should throw an error + [true, () => new CacheMap2('a')] + ]); + + for (const [throws, create] of data) { + if (throws) { + assert.throws(create); + } else { + assert.doesNotThrow(create); + } + } +} + +/** */ +function testApi() { + const data = [ + { + maxSize: 1, + expectedSize: 0, + calls: [] + }, + { + maxSize: 10, + expectedSize: 1, + calls: [ + {func: 'get', args: ['a1-b-c'], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 32}, + {func: 'has', args: ['a1-b-c'], returnValue: true} + ] + }, + { + maxSize: 10, + expectedSize: 2, + calls: [ + {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 32}, + {func: 'set', args: ['a1-b-c', 64], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 64}, + {func: 'set', args: ['a2-b-c', 96], returnValue: void 0}, + {func: 'get', args: ['a2-b-c'], returnValue: 96} + ] + }, + { + maxSize: 2, + expectedSize: 2, + calls: [ + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'has', args: ['a2-b-c'], returnValue: false}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a1-b-c', 1], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: true}, + {func: 'has', args: ['a2-b-c'], returnValue: false}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a2-b-c', 2], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: true}, + {func: 'has', args: ['a2-b-c'], returnValue: true}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a3-b-c', 3], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'has', args: ['a2-b-c'], returnValue: true}, + {func: 'has', args: ['a3-b-c'], returnValue: true} + ] + } + ]; + + for (const {maxSize, expectedSize, calls} of data) { + const cache = new CacheMap2(maxSize); + assert.strictEqual(cache.maxSize, maxSize); + for (const call of calls) { + const {func, args} = call; + let returnValue; + switch (func) { + case 'get': returnValue = cache.get(args[0]); break; + case 'set': returnValue = cache.set(args[0], args[1]); break; + case 'has': returnValue = cache.has(args[0]); break; + case 'clear': returnValue = cache.clear(); break; + } + if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) { + const {returnValue: expectedReturnValue} = call; + assert.deepStrictEqual(returnValue, expectedReturnValue); + } + } + assert.strictEqual(cache.size, expectedSize); + } +} + + +/** */ +function main() { + testConstructor(); + testApi(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-core.js b/test/test-core.js new file mode 100644 index 00000000..2c48ac20 --- /dev/null +++ b/test/test-core.js @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM(); +vm.execute([ + 'js/core.js', + 'js/core/extension-error.js' +]); +const values = vm.get(['DynamicProperty', 'deepEqual']); +const DynamicProperty2 = /** @type {typeof DynamicProperty} */ (values[0]); +const deepEqual2 = /** @type {typeof deepEqual} */ (values[1]); + + +/** */ +function testDynamicProperty() { + const data = [ + { + initialValue: 0, + /** @type {{operation: ?string, expectedDefaultValue: number, expectedValue: number, expectedOverrideCount: number, expeectedEventOccurred: boolean, args: [value: number, priority?: number]}[]} */ + operations: [ + { + operation: null, + args: [0], + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: false + }, + { + operation: 'set.defaultValue', + args: [1], + expectedDefaultValue: 1, + expectedValue: 1, + expectedOverrideCount: 0, + expeectedEventOccurred: true + }, + { + operation: 'set.defaultValue', + args: [1], + expectedDefaultValue: 1, + expectedValue: 1, + expectedOverrideCount: 0, + expeectedEventOccurred: false + }, + { + operation: 'set.defaultValue', + args: [0], + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [8], + expectedDefaultValue: 0, + expectedValue: 8, + expectedOverrideCount: 1, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [16], + expectedDefaultValue: 0, + expectedValue: 8, + expectedOverrideCount: 2, + expeectedEventOccurred: false + }, + { + operation: 'setOverride', + args: [32, 1], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 3, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [64, -1], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 4, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-4], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 3, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-3], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 2, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-2], + expectedDefaultValue: 0, + expectedValue: 64, + expectedOverrideCount: 1, + expeectedEventOccurred: true + }, + { + operation: 'clearOverride', + args: [-1], + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: true + } + ] + } + ]; + + for (const {initialValue, operations} of data) { + const property = new DynamicProperty2(initialValue); + const overrideTokens = []; + let eventOccurred = false; + const onChange = () => { eventOccurred = true; }; + property.on('change', onChange); + for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) { + eventOccurred = false; + switch (operation) { + case 'set.defaultValue': property.defaultValue = args[0]; break; + case 'setOverride': overrideTokens.push(property.setOverride(...args)); break; + case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break; + } + assert.strictEqual(eventOccurred, expeectedEventOccurred); + assert.strictEqual(property.defaultValue, expectedDefaultValue); + assert.strictEqual(property.value, expectedValue); + assert.strictEqual(property.overrideCount, expectedOverrideCount); + } + property.off('change', onChange); + } +} + +/** */ +function testDeepEqual() { + const data = [ + // Simple tests + { + value1: 0, + value2: 0, + expected: true + }, + { + value1: null, + value2: null, + expected: true + }, + { + value1: 'test', + value2: 'test', + expected: true + }, + { + value1: true, + value2: true, + expected: true + }, + { + value1: 0, + value2: 1, + expected: false + }, + { + value1: null, + value2: false, + expected: false + }, + { + value1: 'test1', + value2: 'test2', + expected: false + }, + { + value1: true, + value2: false, + expected: false + }, + + // Simple object tests + { + value1: {}, + value2: {}, + expected: true + }, + { + value1: {}, + value2: [], + expected: false + }, + { + value1: [], + value2: [], + expected: true + }, + { + value1: {}, + value2: null, + expected: false + }, + + // Complex object tests + { + value1: [1], + value2: [], + expected: false + }, + { + value1: [1], + value2: [1], + expected: true + }, + { + value1: [1], + value2: [2], + expected: false + }, + + { + value1: {}, + value2: {test: 1}, + expected: false + }, + { + value1: {test: 1}, + value2: {test: 1}, + expected: true + }, + { + value1: {test: 1}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: true}}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: [true]}}, + value2: {test: {test2: [true]}}, + expected: true + }, + + // Recursive + { + value1: (() => { const x = {}; x.x = x; return x; })(), + value2: (() => { const x = {}; x.x = x; return x; })(), + expected: false + } + ]; + + let index = 0; + for (const {value1, value2, expected} of data) { + const actual1 = deepEqual2(value1, value2); + assert.strictEqual(actual1, expected, `Failed for test ${index}`); + + const actual2 = deepEqual2(value2, value1); + assert.strictEqual(actual2, expected, `Failed for test ${index}`); + + ++index; + } +} + + +/** */ +function main() { + testDynamicProperty(); + testDeepEqual(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-database.js b/test/test-database.js new file mode 100644 index 00000000..947e369b --- /dev/null +++ b/test/test-database.js @@ -0,0 +1,982 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const path = require('path'); +const assert = require('assert'); +const {createDictionaryArchive, testMain} = require('../dev/util'); +const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('../dev/database-vm'); + + +const vm = new DatabaseVM(); +vm.execute([ + 'js/core.js', + 'js/core/extension-error.js', + 'js/general/cache-map.js', + 'js/data/json-schema.js', + 'js/media/media-util.js', + 'js/language/dictionary-importer.js', + 'js/data/database.js', + 'js/language/dictionary-database.js' +]); +/** @type {typeof DictionaryImporter} */ +const DictionaryImporter2 = vm.getSingle('DictionaryImporter'); +/** @type {typeof DictionaryDatabase} */ +const DictionaryDatabase2 = vm.getSingle('DictionaryDatabase'); + + +/** + * @param {string} dictionary + * @param {string} [dictionaryName] + * @returns {import('jszip')} + */ +function createTestDictionaryArchive(dictionary, dictionaryName) { + const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary); + return createDictionaryArchive(dictionaryDirectory, dictionaryName); +} + + +/** + * @param {import('dictionary-importer').OnProgressCallback} [onProgress] + * @returns {DictionaryImporter} + */ +function createDictionaryImporter(onProgress) { + const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader(); + return new DictionaryImporter2(dictionaryImporterMediaLoader, (...args) => { + const {stepIndex, stepCount, index, count} = args[0]; + assert.ok(stepIndex < stepCount); + assert.ok(index <= count); + if (typeof onProgress === 'function') { + onProgress(...args); + } + }); +} + + +/** + * @param {import('dictionary-database').TermEntry[]} dictionaryDatabaseEntries + * @param {string} term + * @returns {number} + */ +function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) { + return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0); +} + +/** + * @param {import('dictionary-database').TermEntry[]} dictionaryDatabaseEntries + * @param {string} reading + * @returns {number} + */ +function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) { + return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0); +} + +/** + * @param {import('dictionary-database').TermMeta[]|import('dictionary-database').KanjiMeta[]} metas + * @param {import('dictionary-database').TermMetaType|import('dictionary-database').KanjiMetaType} mode + * @returns {number} + */ +function countMetasWithMode(metas, mode) { + let i = 0; + for (const item of metas) { + if (item.mode === mode) { ++i; } + } + return i; +} + +/** + * @param {import('dictionary-database').KanjiEntry[]} kanji + * @param {string} character + * @returns {number} + */ +function countKanjiWithCharacter(kanji, character) { + let i = 0; + for (const item of kanji) { + if (item.character === character) { ++i; } + } + return i; +} + + +/** + * @param {number} timeout + * @returns {Promise<void>} + */ +function clearDatabase(timeout) { + return new Promise((resolve, reject) => { + /** @type {?number} */ + let timer = setTimeout(() => { + timer = null; + reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`)); + }, timeout); + + (async () => { + const indexedDB = vm.indexedDB; + for (const {name} of await indexedDB.databases()) { + if (typeof name !== 'string') { continue; } + /** @type {Promise<void>} */ + const promise2 = new Promise((resolve2, reject2) => { + const request = indexedDB.deleteDatabase(name); + request.onerror = (e) => reject2(e); + request.onsuccess = () => resolve2(); + }); + await promise2; + } + if (timer !== null) { + clearTimeout(timer); + } + resolve(); + })(); + }); +} + + +/** */ +async function testDatabase1() { + // Load dictionary data + const testDictionary = createTestDictionaryArchive('valid-dictionary1'); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + + const title = testDictionaryIndex.title; + const titles = new Map([ + [title, {priority: 0, allowSecondarySearches: false}] + ]); + + // Setup iteration data + const iterations = [ + { + cleanup: async () => { + // Test purge + await dictionaryDatabase.purge(); + await testDatabaseEmpty1(dictionaryDatabase); + } + }, + { + cleanup: async () => { + // Test deleteDictionary + let progressEvent = false; + await dictionaryDatabase.deleteDictionary( + title, + 1000, + () => { + progressEvent = true; + } + ); + assert.ok(progressEvent); + + await testDatabaseEmpty1(dictionaryDatabase); + } + }, + { + cleanup: async () => {} + } + ]; + + // Setup database + const dictionaryDatabase = new DictionaryDatabase2(); + await dictionaryDatabase.prepare(); + + for (const {cleanup} of iterations) { + const expectedSummary = { + title, + revision: 'test', + sequenced: true, + version: 3, + importDate: 0, + prefixWildcardsSupported: true, + counts: { + kanji: {total: 2}, + kanjiMeta: {total: 6, freq: 6}, + media: {total: 4}, + tagMeta: {total: 15}, + termMeta: {total: 38, freq: 31, pitch: 7}, + terms: {total: 21} + } + }; + + // Import data + let progressEvent = false; + const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; }); + const {result, errors} = await dictionaryImporter.importDictionary( + dictionaryDatabase, + testDictionarySource, + {prefixWildcardsSupported: true} + ); + expectedSummary.importDate = result.importDate; + vm.assert.deepStrictEqual(errors, []); + vm.assert.deepStrictEqual(result, expectedSummary); + assert.ok(progressEvent); + + // Get info summary + const info = await dictionaryDatabase.getDictionaryInfo(); + vm.assert.deepStrictEqual(info, [expectedSummary]); + + // Get counts + const counts = await dictionaryDatabase.getDictionaryCounts( + info.map((v) => v.title), + true + ); + vm.assert.deepStrictEqual(counts, { + counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}], + total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4} + }); + + // Test find* functions + await testFindTermsBulkTest1(dictionaryDatabase, titles); + await testTindTermsExactBulk1(dictionaryDatabase, titles); + await testFindTermsBySequenceBulk1(dictionaryDatabase, title); + await testFindTermMetaBulk1(dictionaryDatabase, titles); + await testFindKanjiBulk1(dictionaryDatabase, titles); + await testFindKanjiMetaBulk1(dictionaryDatabase, titles); + await testFindTagForTitle1(dictionaryDatabase, title); + + // Cleanup + await cleanup(); + } + + await dictionaryDatabase.close(); +} + +/** + * @param {DictionaryDatabase} database + */ +async function testDatabaseEmpty1(database) { + const info = await database.getDictionaryInfo(); + vm.assert.deepStrictEqual(info, []); + + const counts = await database.getDictionaryCounts([], true); + vm.assert.deepStrictEqual(counts, { + counts: [], + total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0} + }); +} + +/** + * @param {DictionaryDatabase} database + * @param {import('dictionary-database').DictionarySet} titles + */ +async function testFindTermsBulkTest1(database, titles) { + /** @type {{inputs: {matchType: import('dictionary-database').MatchType, termList: string[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + matchType: 'exact', + termList: ['打', '打つ', '打ち込む'] + }, + { + matchType: 'exact', + termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ'] + }, + { + matchType: 'prefix', + termList: ['打'] + } + ], + expectedResults: { + total: 10, + terms: [ + ['打', 2], + ['打つ', 4], + ['打ち込む', 4] + ], + readings: [ + ['だ', 1], + ['ダース', 1], + ['うつ', 2], + ['ぶつ', 2], + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + matchType: 'exact', + termList: ['込む'] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + matchType: 'suffix', + termList: ['込む'] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打ち込む', 4] + ], + readings: [ + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + matchType: 'exact', + termList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList, matchType} of inputs) { + const results = await database.findTermsBulk(termList, titles, matchType); + assert.strictEqual(results.length, expectedResults.total); + for (const [term, count] of expectedResults.terms) { + assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); + } + for (const [reading, count] of expectedResults.readings) { + assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {import('dictionary-database').DictionarySet} titles + */ +async function testTindTermsExactBulk1(database, titles) { + /** @type {{inputs: {termList: {term: string, reading: string}[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + termList: [ + {term: '打', reading: 'だ'}, + {term: '打つ', reading: 'うつ'}, + {term: '打ち込む', reading: 'うちこむ'} + ] + } + ], + expectedResults: { + total: 5, + terms: [ + ['打', 1], + ['打つ', 2], + ['打ち込む', 2] + ], + readings: [ + ['だ', 1], + ['うつ', 2], + ['うちこむ', 2] + ] + } + }, + { + inputs: [ + { + termList: [ + {term: '打', reading: 'だ?'}, + {term: '打つ', reading: 'うつ?'}, + {term: '打ち込む', reading: 'うちこむ?'} + ] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + termList: [ + {term: '打つ', reading: 'うつ'}, + {term: '打つ', reading: 'ぶつ'} + ] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打つ', 4] + ], + readings: [ + ['うつ', 2], + ['ぶつ', 2] + ] + } + }, + { + inputs: [ + { + termList: [ + {term: '打つ', reading: 'うちこむ'} + ] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + termList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList} of inputs) { + const results = await database.findTermsExactBulk(termList, titles); + assert.strictEqual(results.length, expectedResults.total); + for (const [term, count] of expectedResults.terms) { + assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); + } + for (const [reading, count] of expectedResults.readings) { + assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {string} mainDictionary + */ +async function testFindTermsBySequenceBulk1(database, mainDictionary) { + /** @type {{inputs: {sequenceList: number[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + sequenceList: [1, 2, 3, 4, 5] + } + ], + expectedResults: { + total: 11, + terms: [ + ['打', 2], + ['打つ', 4], + ['打ち込む', 4], + ['画像', 1] + ], + readings: [ + ['だ', 1], + ['ダース', 1], + ['うつ', 2], + ['ぶつ', 2], + ['うちこむ', 2], + ['ぶちこむ', 2], + ['がぞう', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [1] + } + ], + expectedResults: { + total: 1, + terms: [ + ['打', 1] + ], + readings: [ + ['だ', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [2] + } + ], + expectedResults: { + total: 1, + terms: [ + ['打', 1] + ], + readings: [ + ['ダース', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [3] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打つ', 4] + ], + readings: [ + ['うつ', 2], + ['ぶつ', 2] + ] + } + }, + { + inputs: [ + { + sequenceList: [4] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打ち込む', 4] + ], + readings: [ + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + sequenceList: [5] + } + ], + expectedResults: { + total: 1, + terms: [ + ['画像', 1] + ], + readings: [ + ['がぞう', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [-1] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + sequenceList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {sequenceList} of inputs) { + const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary}))); + assert.strictEqual(results.length, expectedResults.total); + for (const [term, count] of expectedResults.terms) { + assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); + } + for (const [reading, count] of expectedResults.readings) { + assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {import('dictionary-database').DictionarySet} titles + */ +async function testFindTermMetaBulk1(database, titles) { + /** @type {{inputs: {termList: string[]}[], expectedResults: {total: number, modes: [key: import('dictionary-database').TermMetaType, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + termList: ['打'] + } + ], + expectedResults: { + total: 11, + modes: [ + ['freq', 11] + ] + } + }, + { + inputs: [ + { + termList: ['打つ'] + } + ], + expectedResults: { + total: 10, + modes: [ + ['freq', 10] + ] + } + }, + { + inputs: [ + { + termList: ['打ち込む'] + } + ], + expectedResults: { + total: 12, + modes: [ + ['freq', 10], + ['pitch', 2] + ] + } + }, + { + inputs: [ + { + termList: ['?'] + } + ], + expectedResults: { + total: 0, + modes: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList} of inputs) { + const results = await database.findTermMetaBulk(termList, titles); + assert.strictEqual(results.length, expectedResults.total); + for (const [mode, count] of expectedResults.modes) { + assert.strictEqual(countMetasWithMode(results, mode), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {import('dictionary-database').DictionarySet} titles + */ +async function testFindKanjiBulk1(database, titles) { + /** @type {{inputs: {kanjiList: string[]}[], expectedResults: {total: number, kanji: [key: string, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + kanjiList: ['打'] + } + ], + expectedResults: { + total: 1, + kanji: [ + ['打', 1] + ] + } + }, + { + inputs: [ + { + kanjiList: ['込'] + } + ], + expectedResults: { + total: 1, + kanji: [ + ['込', 1] + ] + } + }, + { + inputs: [ + { + kanjiList: ['?'] + } + ], + expectedResults: { + total: 0, + kanji: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {kanjiList} of inputs) { + const results = await database.findKanjiBulk(kanjiList, titles); + assert.strictEqual(results.length, expectedResults.total); + for (const [kanji, count] of expectedResults.kanji) { + assert.strictEqual(countKanjiWithCharacter(results, kanji), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {import('dictionary-database').DictionarySet} titles + */ +async function testFindKanjiMetaBulk1(database, titles) { + /** @type {{inputs: {kanjiList: string[]}[], expectedResults: {total: number, modes: [key: import('dictionary-database').KanjiMetaType, count: number][]}}[]} */ + const data = [ + { + inputs: [ + { + kanjiList: ['打'] + } + ], + expectedResults: { + total: 3, + modes: [ + ['freq', 3] + ] + } + }, + { + inputs: [ + { + kanjiList: ['込'] + } + ], + expectedResults: { + total: 3, + modes: [ + ['freq', 3] + ] + } + }, + { + inputs: [ + { + kanjiList: ['?'] + } + ], + expectedResults: { + total: 0, + modes: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {kanjiList} of inputs) { + const results = await database.findKanjiMetaBulk(kanjiList, titles); + assert.strictEqual(results.length, expectedResults.total); + for (const [mode, count] of expectedResults.modes) { + assert.strictEqual(countMetasWithMode(results, mode), count); + } + } + } +} + +/** + * @param {DictionaryDatabase} database + * @param {string} title + */ +async function testFindTagForTitle1(database, title) { + const data = [ + { + inputs: [ + { + name: 'E1' + } + ], + expectedResults: { + value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'K1' + } + ], + expectedResults: { + value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'kstat1' + } + ], + expectedResults: { + value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'invalid' + } + ], + expectedResults: { + value: null + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {name} of inputs) { + const result = await database.findTagForTitle(name, title); + vm.assert.deepStrictEqual(result, expectedResults.value); + } + } +} + + +/** */ +async function testDatabase2() { + // Load dictionary data + const testDictionary = createTestDictionaryArchive('valid-dictionary1'); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + + const title = testDictionaryIndex.title; + const titles = new Map([ + [title, {priority: 0, allowSecondarySearches: false}] + ]); + + // Setup database + const dictionaryDatabase = new DictionaryDatabase2(); + /** @type {import('dictionary-importer').ImportDetails} */ + const detaultImportDetails = {prefixWildcardsSupported: false}; + + // Error: not prepared + await assert.rejects(async () => await dictionaryDatabase.deleteDictionary(title, 1000, () => {})); + await assert.rejects(async () => await dictionaryDatabase.findTermsBulk(['?'], titles, 'exact')); + await assert.rejects(async () => await dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles)); + await assert.rejects(async () => await dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}])); + await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles)); + await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles)); + await assert.rejects(async () => await dictionaryDatabase.findKanjiBulk(['?'], titles)); + await assert.rejects(async () => await dictionaryDatabase.findKanjiMetaBulk(['?'], titles)); + await assert.rejects(async () => await dictionaryDatabase.findTagForTitle('tag', title)); + await assert.rejects(async () => await dictionaryDatabase.getDictionaryInfo()); + await assert.rejects(async () => await dictionaryDatabase.getDictionaryCounts([...titles.keys()], true)); + await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails)); + + await dictionaryDatabase.prepare(); + + // Error: already prepared + await assert.rejects(async () => await dictionaryDatabase.prepare()); + + await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails); + + // Error: dictionary already imported + await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails)); + + await dictionaryDatabase.close(); +} + + +/** */ +async function testDatabase3() { + const invalidDictionaries = [ + 'invalid-dictionary1', + 'invalid-dictionary2', + 'invalid-dictionary3', + 'invalid-dictionary4', + 'invalid-dictionary5', + 'invalid-dictionary6' + ]; + + // Setup database + const dictionaryDatabase = new DictionaryDatabase2(); + /** @type {import('dictionary-importer').ImportDetails} */ + const detaultImportDetails = {prefixWildcardsSupported: false}; + await dictionaryDatabase.prepare(); + + for (const invalidDictionary of invalidDictionaries) { + const testDictionary = createTestDictionaryArchive(invalidDictionary); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + + let error = null; + try { + await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails); + } catch (e) { + error = e; + } + + if (error === null) { + assert.ok(false, `Expected an error while importing ${invalidDictionary}`); + } else { + const prefix = 'Dictionary has invalid data'; + const message = /** @type {import('core').UnknownObject} */ (error).message; + assert.ok(typeof message, 'string'); + if (typeof message === 'string') { + assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`); + } + } + } + + await dictionaryDatabase.close(); +} + + +/** */ +async function main() { + const clearTimeout = 5000; + try { + await testDatabase1(); + await clearDatabase(clearTimeout); + + await testDatabase2(); + await clearDatabase(clearTimeout); + + await testDatabase3(); + await clearDatabase(clearTimeout); + } catch (e) { + console.log(e); + process.exit(-1); + throw e; + } +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-document-util.js b/test/test-document-util.js new file mode 100644 index 00000000..93ce1669 --- /dev/null +++ b/test/test-document-util.js @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {JSDOM} = require('jsdom'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + + +// DOMRect class definition +class DOMRect { + /** + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + */ + constructor(x, y, width, height) { + /** @type {number} */ + this._x = x; + /** @type {number} */ + this._y = y; + /** @type {number} */ + this._width = width; + /** @type {number} */ + this._height = height; + } + + /** @type {number} */ + get x() { return this._x; } + /** @type {number} */ + get y() { return this._y; } + /** @type {number} */ + get width() { return this._width; } + /** @type {number} */ + get height() { return this._height; } + /** @type {number} */ + get left() { return this._x + Math.min(0, this._width); } + /** @type {number} */ + get right() { return this._x + Math.max(0, this._width); } + /** @type {number} */ + get top() { return this._y + Math.min(0, this._height); } + /** @type {number} */ + get bottom() { return this._y + Math.max(0, this._height); } + /** @returns {string} */ + toJSON() { return '<not implemented>'; } +} + + +/** + * @param {string} fileName + * @returns {JSDOM} + */ +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + const dom = new JSDOM(domSource); + const document = dom.window.document; + const window = dom.window; + + // Define innerText setter as an alias for textContent setter + Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', { + set(value) { this.textContent = value; } + }); + + // Placeholder for feature detection + document.caretRangeFromPoint = () => null; + + return dom; +} + +/** + * @param {Element} element + * @param {string|undefined} selector + * @returns {?Element} + */ +function querySelectorChildOrSelf(element, selector) { + return selector ? element.querySelector(selector) : element; +} + +/** + * @param {JSDOM} dom + * @param {?Node} node + * @returns {?Text|Node} + */ +function getChildTextNodeOrSelf(dom, node) { + if (node === null) { return null; } + const Node = dom.window.Node; + const childNode = node.firstChild; + return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node); +} + +/** + * @param {unknown} value + * @returns {unknown} + */ +function getPrototypeOfOrNull(value) { + try { + return Object.getPrototypeOf(value); + } catch (e) { + return null; + } +} + +/** + * @param {Document} document + * @returns {?Element} + */ +function findImposterElement(document) { + // Finds the imposter element based on it's z-index style + return document.querySelector('div[style*="2147483646"]>*'); +} + + +/** */ +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html')); + const window = dom.window; + const document = window.document; + const Node = window.Node; + const Range = window.Range; + + const vm = new VM({document, window, Range, Node}); + vm.execute([ + 'js/data/sandbox/string-util.js', + 'js/dom/dom-text-scanner.js', + 'js/dom/text-source-range.js', + 'js/dom/text-source-element.js', + 'js/dom/document-util.js' + ]); + /** @type {[DOMTextScanner: typeof DOMTextScanner, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement, DocumentUtil: typeof DocumentUtil]} */ + const [DOMTextScanner2, TextSourceRange2, TextSourceElement2, DocumentUtil2] = vm.get([ + 'DOMTextScanner', + 'TextSourceRange', + 'TextSourceElement', + 'DocumentUtil' + ]); + + try { + await testDocumentTextScanningFunctions(dom, {DocumentUtil: DocumentUtil2, TextSourceRange: TextSourceRange2, TextSourceElement: TextSourceElement2}); + await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner: DOMTextScanner2}); + } finally { + window.close(); + } +} + +/** + * @param {JSDOM} dom + * @param {{DocumentUtil: typeof DocumentUtil, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement}} details + */ +async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) { + const document = dom.window.document; + + for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=scan]'))) { + // Get test parameters + const { + elementFromPointSelector, + caretRangeFromPointSelector, + startNodeSelector, + startOffset, + endNodeSelector, + endOffset, + resultType, + sentenceScanExtent, + sentence, + hasImposter, + terminateAtNewlines + } = testElement.dataset; + + const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector); + const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector); + const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector)); + const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector)); + + const startOffset2 = parseInt(/** @type {string} */ (startOffset), 10); + const endOffset2 = parseInt(/** @type {string} */ (endOffset), 10); + const sentenceScanExtent2 = parseInt(/** @type {string} */ (sentenceScanExtent), 10); + const terminateAtNewlines2 = (terminateAtNewlines !== 'false'); + + assert.notStrictEqual(elementFromPointValue, null); + assert.notStrictEqual(caretRangeFromPointValue, null); + assert.notStrictEqual(startNode, null); + assert.notStrictEqual(endNode, null); + + // Setup functions + document.elementFromPoint = () => elementFromPointValue; + + document.caretRangeFromPoint = (x, y) => { + const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document)); + assert.strictEqual(!!imposter, hasImposter === 'true'); + + const range = document.createRange(); + range.setStart(/** @type {Node} */ (imposter ? imposter : startNode), startOffset2); + range.setEnd(/** @type {Node} */ (imposter ? imposter : startNode), endOffset2); + + // Override getClientRects to return a rect guaranteed to contain (x, y) + range.getClientRects = () => { + /** @type {import('test/document-types').PseudoDOMRectList} */ + const domRectList = Object.assign( + [new DOMRect(x - 1, y - 1, 2, 2)], + { + /** + * @this {DOMRect[]} + * @param {number} index + * @returns {DOMRect} + */ + item: function item(index) { return this[index]; } + } + ); + return domRectList; + }; + return range; + }; + + // Test docRangeFromPoint + const source = DocumentUtil.getRangeFromPoint(0, 0, { + deepContentScan: false, + normalizeCssZoom: true + }); + switch (resultType) { + case 'TextSourceRange': + assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype); + break; + case 'TextSourceElement': + assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype); + break; + case 'null': + assert.strictEqual(source, null); + break; + default: + assert.ok(false); + break; + } + if (source === null) { continue; } + + // Sentence info + const terminatorString = '…。..??!!'; + const terminatorMap = new Map(); + for (const char of terminatorString) { + terminatorMap.set(char, [false, true]); + } + const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']]; + const forwardQuoteMap = new Map(); + const backwardQuoteMap = new Map(); + for (const [char1, char2] of quoteArray) { + forwardQuoteMap.set(char1, [char2, false]); + backwardQuoteMap.set(char2, [char1, false]); + } + + // Test docSentenceExtract + const sentenceActual = DocumentUtil.extractSentence( + source, + false, + sentenceScanExtent2, + terminateAtNewlines2, + terminatorMap, + forwardQuoteMap, + backwardQuoteMap + ).text; + assert.strictEqual(sentenceActual, sentence); + + // Clean + source.cleanup(); + } +} + +/** + * @param {JSDOM} dom + * @param {{DOMTextScanner: typeof DOMTextScanner}} details + */ +async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) { + const document = dom.window.document; + + for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=text-source-range-seek]'))) { + // Get test parameters + const { + seekNodeSelector, + seekNodeIsText, + seekOffset, + seekLength, + seekDirection, + expectedResultNodeSelector, + expectedResultNodeIsText, + expectedResultOffset, + expectedResultContent + } = testElement.dataset; + + const seekOffset2 = parseInt(/** @type {string} */ (seekOffset), 10); + const seekLength2 = parseInt(/** @type {string} */ (seekLength), 10); + const expectedResultOffset2 = parseInt(/** @type {string} */ (expectedResultOffset), 10); + + /** @type {?Node} */ + let seekNode = testElement.querySelector(/** @type {string} */ (seekNodeSelector)); + if (seekNodeIsText === 'true' && seekNode !== null) { + seekNode = seekNode.firstChild; + } + + /** @type {?Node} */ + let expectedResultNode = testElement.querySelector(/** @type {string} */ (expectedResultNodeSelector)); + if (expectedResultNodeIsText === 'true' && expectedResultNode !== null) { + expectedResultNode = expectedResultNode.firstChild; + } + + const {node, offset, content} = ( + seekDirection === 'forward' ? + new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(seekLength2) : + new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(-seekLength2) + ); + + assert.strictEqual(node, expectedResultNode); + assert.strictEqual(offset, expectedResultOffset2); + assert.strictEqual(content, expectedResultContent); + } +} + + +/** */ +async function main() { + await testDocument1(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-hotkey-util.js b/test/test-hotkey-util.js new file mode 100644 index 00000000..88f5a8d2 --- /dev/null +++ b/test/test-hotkey-util.js @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + + +/** + * @template T + * @param {T} value + * @returns {T} + */ +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +/** + * @returns {HotkeyUtil} + */ +function createHotkeyUtil() { + const vm = new VM(); + vm.execute(['js/input/hotkey-util.js']); + /** @type {typeof HotkeyUtil} */ + const HotkeyUtil2 = vm.getSingle('HotkeyUtil'); + return new HotkeyUtil2(); +} + + +/** */ +function testCommandConversions() { + /** @type {{os: import('environment').OperatingSystem, command: string, expectedCommand: string, expectedInput: {key: string, modifiers: import('input').Modifier[]}}[]} */ + const data = [ + {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}}, + {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}}, + + {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + + {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + + {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}} + ]; + + const hotkeyUtil = createHotkeyUtil(); + for (const {command, os, expectedInput, expectedCommand} of data) { + hotkeyUtil.os = os; + const input = clone(hotkeyUtil.convertCommandToInput(command)); + assert.deepStrictEqual(input, expectedInput); + const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers); + assert.deepStrictEqual(command2, expectedCommand); + } +} + +/** */ +function testDisplayNames() { + /** @type {{os: import('environment').OperatingSystem, key: ?string, modifiers: import('input').Modifier[], expected: string}[]} */ + const data = [ + {os: 'win', key: null, modifiers: [], expected: ''}, + {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'win', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'}, + {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'}, + {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'}, + {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'mac', key: null, modifiers: [], expected: ''}, + {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'}, + {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'}, + {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'}, + {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'}, + {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'}, + {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'}, + {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'linux', key: null, modifiers: [], expected: ''}, + {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'}, + {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'}, + {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'}, + {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'unknown', key: null, modifiers: [], expected: ''}, + {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'}, + {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'}, + {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'}, + {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'} + ]; + + const hotkeyUtil = createHotkeyUtil(); + for (const {os, key, modifiers, expected} of data) { + hotkeyUtil.os = os; + const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers); + assert.deepStrictEqual(displayName, expected); + } +} + +/** */ +function testSortModifiers() { + /** @type {{modifiers: import('input').Modifier[], expected: import('input').Modifier[]}[]} */ + const data = [ + {modifiers: [], expected: []}, + {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']} + ]; + + const hotkeyUtil = createHotkeyUtil(); + for (const {modifiers, expected} of data) { + const modifiers2 = hotkeyUtil.sortModifiers(modifiers); + assert.strictEqual(modifiers2, modifiers); + assert.deepStrictEqual(modifiers2, expected); + } +} + + +/** */ +function main() { + testCommandConversions(); + testDisplayNames(); + testSortModifiers(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-japanese-util.js b/test/test-japanese-util.js new file mode 100644 index 00000000..0a95b858 --- /dev/null +++ b/test/test-japanese-util.js @@ -0,0 +1,915 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM(); +vm.execute([ + 'lib/wanakana.min.js', + 'js/language/sandbox/japanese-util.js', + 'js/general/text-source-map.js' +]); +/** @type {[JapaneseUtil: typeof JapaneseUtil, TextSourceMap: typeof TextSourceMap, wanakana: import('wanakana')]} */ +const [JapaneseUtil2, TextSourceMap2, wanakana] = vm.get(['JapaneseUtil', 'TextSourceMap', 'wanakana']); +const jp = new JapaneseUtil2(wanakana); + + +/** */ +function testIsCodePointKanji() { + /** @type {[characters: string, expected: boolean][]} */ + const data = [ + ['力方', true], + ['\u53f1\u{20b9f}', true], + ['かたカタ々kata、。?,.?', false], + ['逸逸', true] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = /** @type {number} */ (character.codePointAt(0)); + const actual = jp.isCodePointKanji(codePoint); + assert.strictEqual(actual, expected, `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})`); + } + } +} + +/** */ +function testIsCodePointKana() { + /** @type {[characters: string, expected: boolean][]} */ + const data = [ + ['かたカタ', true], + ['力方々kata、。?,.?', false], + ['\u53f1\u{20b9f}', false] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = /** @type {number} */ (character.codePointAt(0)); + const actual = jp.isCodePointKana(codePoint); + assert.strictEqual(actual, expected, `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})`); + } + } +} + +/** */ +function testIsCodePointJapanese() { + /** @type {[characters: string, expected: boolean][]} */ + const data = [ + ['かたカタ力方々、。?', true], + ['\u53f1\u{20b9f}', true], + ['kata,.?', false], + ['逸逸', true] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = /** @type {number} */ (character.codePointAt(0)); + const actual = jp.isCodePointJapanese(codePoint); + assert.strictEqual(actual, expected, `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})`); + } + } +} + +/** */ +function testIsStringEntirelyKana() { + /** @type {[string: string, expected: boolean][]} */ + const data = [ + ['かたかな', true], + ['カタカナ', true], + ['ひらがな', true], + ['ヒラガナ', true], + ['カタカナひらがな', true], + ['かたカタ力方々、。?', false], + ['\u53f1\u{20b9f}', false], + ['kata,.?', false], + ['かたカタ力方々、。?invalid', false], + ['\u53f1\u{20b9f}invalid', false], + ['kata,.?かた', false] + ]; + + for (const [string, expected] of data) { + assert.strictEqual(jp.isStringEntirelyKana(string), expected); + } +} + +/** */ +function testIsStringPartiallyJapanese() { + /** @type {[string: string, expected: boolean][]} */ + const data = [ + ['かたかな', true], + ['カタカナ', true], + ['ひらがな', true], + ['ヒラガナ', true], + ['カタカナひらがな', true], + ['かたカタ力方々、。?', true], + ['\u53f1\u{20b9f}', true], + ['kata,.?', false], + ['かたカタ力方々、。?invalid', true], + ['\u53f1\u{20b9f}invalid', true], + ['kata,.?かた', true], + ['逸逸', true] + ]; + + for (const [string, expected] of data) { + assert.strictEqual(jp.isStringPartiallyJapanese(string), expected); + } +} + +/** */ +function testConvertKatakanaToHiragana() { + /** @type {[string: string, expected: string, keepProlongedSoundMarks?: boolean][]} */ + const data = [ + ['かたかな', 'かたかな'], + ['ひらがな', 'ひらがな'], + ['カタカナ', 'かたかな'], + ['ヒラガナ', 'ひらがな'], + ['カタカナかたかな', 'かたかなかたかな'], + ['ヒラガナひらがな', 'ひらがなひらがな'], + ['chikaraちからチカラ力', 'chikaraちからちから力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'], + ['カーナー', 'かあなあ'], + ['カーナー', 'かーなー', true] + ]; + + for (const [string, expected, keepProlongedSoundMarks=false] of data) { + assert.strictEqual(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks), expected); + } +} + +/** */ +function testConvertHiraganaToKatakana() { + /** @type {[string: string, expected: string][]} */ + const data = [ + ['かたかな', 'カタカナ'], + ['ひらがな', 'ヒラガナ'], + ['カタカナ', 'カタカナ'], + ['ヒラガナ', 'ヒラガナ'], + ['カタカナかたかな', 'カタカナカタカナ'], + ['ヒラガナひらがな', 'ヒラガナヒラガナ'], + ['chikaraちからチカラ力', 'chikaraチカラチカラ力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'] + ]; + + for (const [string, expected] of data) { + assert.strictEqual(jp.convertHiraganaToKatakana(string), expected); + } +} + +/** */ +function testConvertToRomaji() { + /** @type {[string: string, expected: string][]} */ + const data = [ + ['かたかな', 'katakana'], + ['ひらがな', 'hiragana'], + ['カタカナ', 'katakana'], + ['ヒラガナ', 'hiragana'], + ['カタカナかたかな', 'katakanakatakana'], + ['ヒラガナひらがな', 'hiraganahiragana'], + ['chikaraちからチカラ力', 'chikarachikarachikara力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'] + ]; + + for (const [string, expected] of data) { + assert.strictEqual(jp.convertToRomaji(string), expected); + } +} + +/** */ +function testConvertNumericToFullWidth() { + /** @type {[string: string, expected: string][]} */ + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'abcdefghij'], + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'] + ]; + + for (const [string, expected] of data) { + assert.strictEqual(jp.convertNumericToFullWidth(string), expected); + } +} + +/** */ +function testConvertHalfWidthKanaToFullWidth() { + /** @type {[string: string, expected: string, expectedSourceMapping?: number[]][]} */ + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'abcdefghij'], + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'], + ['カキ', 'カキ', [1, 1]], + ['ガキ', 'ガキ', [2, 1]], + ['ニホン', 'ニホン', [1, 1, 1]], + ['ニッポン', 'ニッポン', [1, 1, 2, 1]] + ]; + + for (const [string, expected, expectedSourceMapping] of data) { + const sourceMap = new TextSourceMap2(string); + const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null); + const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap); + assert.strictEqual(actual1, expected); + assert.strictEqual(actual2, expected); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap2(string, expectedSourceMapping))); + } + } +} + +/** */ +function testConvertAlphabeticToKana() { + /** @type {[string: string, expected: string, expectedSourceMapping?: number[]][]} */ + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], + ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'], + ['chikara', 'ちから', [3, 2, 2]], + ['CHIKARA', 'ちから', [3, 2, 2]] + ]; + + for (const [string, expected, expectedSourceMapping] of data) { + const sourceMap = new TextSourceMap2(string); + const actual1 = jp.convertAlphabeticToKana(string, null); + const actual2 = jp.convertAlphabeticToKana(string, sourceMap); + assert.strictEqual(actual1, expected); + assert.strictEqual(actual2, expected); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap2(string, expectedSourceMapping))); + } + } +} + +/** */ +function testDistributeFurigana() { + /** @type {[input: [term: string, reading: string], expected: {text: string, reading: string}[]][]} */ + const data = [ + ([ + ['有り難う', 'ありがとう'], + [ + {text: '有', reading: 'あ'}, + {text: 'り', reading: ''}, + {text: '難', reading: 'がと'}, + {text: 'う', reading: ''} + ] + ]), + [ + ['方々', 'かたがた'], + [ + {text: '方々', reading: 'かたがた'} + ] + ], + [ + ['お祝い', 'おいわい'], + [ + {text: 'お', reading: ''}, + {text: '祝', reading: 'いわ'}, + {text: 'い', reading: ''} + ] + ], + [ + ['美味しい', 'おいしい'], + [ + {text: '美味', reading: 'おい'}, + {text: 'しい', reading: ''} + ] + ], + [ + ['食べ物', 'たべもの'], + [ + {text: '食', reading: 'た'}, + {text: 'べ', reading: ''}, + {text: '物', reading: 'もの'} + ] + ], + [ + ['試し切り', 'ためしぎり'], + [ + {text: '試', reading: 'ため'}, + {text: 'し', reading: ''}, + {text: '切', reading: 'ぎ'}, + {text: 'り', reading: ''} + ] + ], + // Ambiguous + [ + ['飼い犬', 'かいいぬ'], + [ + {text: '飼い犬', reading: 'かいいぬ'} + ] + ], + [ + ['長い間', 'ながいあいだ'], + [ + {text: '長い間', reading: 'ながいあいだ'} + ] + ], + // Same/empty reading + [ + ['飼い犬', ''], + [ + {text: '飼い犬', reading: ''} + ] + ], + [ + ['かいいぬ', 'かいいぬ'], + [ + {text: 'かいいぬ', reading: ''} + ] + ], + [ + ['かいぬ', 'かいぬ'], + [ + {text: 'かいぬ', reading: ''} + ] + ], + // Misc + [ + ['月', 'か'], + [ + {text: '月', reading: 'か'} + ] + ], + [ + ['月', 'カ'], + [ + {text: '月', reading: 'カ'} + ] + ], + // Mismatched kana readings + [ + ['有り難う', 'アリガトウ'], + [ + {text: '有', reading: 'ア'}, + {text: 'り', reading: 'リ'}, + {text: '難', reading: 'ガト'}, + {text: 'う', reading: 'ウ'} + ] + ], + [ + ['ありがとう', 'アリガトウ'], + [ + {text: 'ありがとう', reading: 'アリガトウ'} + ] + ], + // Mismatched kana readings (real examples) + [ + ['カ月', 'かげつ'], + [ + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['序ノ口', 'じょのくち'], + [ + {text: '序', reading: 'じょ'}, + {text: 'ノ', reading: 'の'}, + {text: '口', reading: 'くち'} + ] + ], + [ + ['スズメの涙', 'すずめのなみだ'], + [ + {text: 'スズメ', reading: 'すずめ'}, + {text: 'の', reading: ''}, + {text: '涙', reading: 'なみだ'} + ] + ], + [ + ['二カ所', 'にかしょ'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['八ツ橋', 'やつはし'], + [ + {text: '八', reading: 'や'}, + {text: 'ツ', reading: 'つ'}, + {text: '橋', reading: 'はし'} + ] + ], + [ + ['八ツ橋', 'やつはし'], + [ + {text: '八', reading: 'や'}, + {text: 'ツ', reading: 'つ'}, + {text: '橋', reading: 'はし'} + ] + ], + [ + ['一カ月', 'いっかげつ'], + [ + {text: '一', reading: 'いっ'}, + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['一カ所', 'いっかしょ'], + [ + {text: '一', reading: 'いっ'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['カ所', 'かしょ'], + [ + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['数カ月', 'すうかげつ'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['くノ一', 'くのいち'], + [ + {text: 'く', reading: ''}, + {text: 'ノ', reading: 'の'}, + {text: '一', reading: 'いち'} + ] + ], + [ + ['くノ一', 'くのいち'], + [ + {text: 'く', reading: ''}, + {text: 'ノ', reading: 'の'}, + {text: '一', reading: 'いち'} + ] + ], + [ + ['数カ国', 'すうかこく'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '国', reading: 'こく'} + ] + ], + [ + ['数カ所', 'すうかしょ'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['壇ノ浦の戦い', 'だんのうらのたたかい'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '戦', reading: 'たたか'}, + {text: 'い', reading: ''} + ] + ], + [ + ['壇ノ浦の戦', 'だんのうらのたたかい'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '戦', reading: 'たたかい'} + ] + ], + [ + ['序ノ口格', 'じょのくちかく'], + [ + {text: '序', reading: 'じょ'}, + {text: 'ノ', reading: 'の'}, + {text: '口格', reading: 'くちかく'} + ] + ], + [ + ['二カ国語', 'にかこくご'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '国語', reading: 'こくご'} + ] + ], + [ + ['カ国', 'かこく'], + [ + {text: 'カ', reading: 'か'}, + {text: '国', reading: 'こく'} + ] + ], + [ + ['カ国語', 'かこくご'], + [ + {text: 'カ', reading: 'か'}, + {text: '国語', reading: 'こくご'} + ] + ], + [ + ['壇ノ浦の合戦', 'だんのうらのかっせん'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '合戦', reading: 'かっせん'} + ] + ], + [ + ['一タ偏', 'いちたへん'], + [ + {text: '一', reading: 'いち'}, + {text: 'タ', reading: 'た'}, + {text: '偏', reading: 'へん'} + ] + ], + [ + ['ル又', 'るまた'], + [ + {text: 'ル', reading: 'る'}, + {text: '又', reading: 'また'} + ] + ], + [ + ['ノ木偏', 'のぎへん'], + [ + {text: 'ノ', reading: 'の'}, + {text: '木偏', reading: 'ぎへん'} + ] + ], + [ + ['一ノ貝', 'いちのかい'], + [ + {text: '一', reading: 'いち'}, + {text: 'ノ', reading: 'の'}, + {text: '貝', reading: 'かい'} + ] + ], + [ + ['虎ノ門事件', 'とらのもんじけん'], + [ + {text: '虎', reading: 'とら'}, + {text: 'ノ', reading: 'の'}, + {text: '門事件', reading: 'もんじけん'} + ] + ], + [ + ['教育ニ関スル勅語', 'きょういくにかんするちょくご'], + [ + {text: '教育', reading: 'きょういく'}, + {text: 'ニ', reading: 'に'}, + {text: '関', reading: 'かん'}, + {text: 'スル', reading: 'する'}, + {text: '勅語', reading: 'ちょくご'} + ] + ], + [ + ['二カ年', 'にかねん'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['三カ年', 'さんかねん'], + [ + {text: '三', reading: 'さん'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['四カ年', 'よんかねん'], + [ + {text: '四', reading: 'よん'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['五カ年', 'ごかねん'], + [ + {text: '五', reading: 'ご'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['六カ年', 'ろっかねん'], + [ + {text: '六', reading: 'ろっ'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['七カ年', 'ななかねん'], + [ + {text: '七', reading: 'なな'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['八カ年', 'はちかねん'], + [ + {text: '八', reading: 'はち'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['九カ年', 'きゅうかねん'], + [ + {text: '九', reading: 'きゅう'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['十カ年', 'じゅうかねん'], + [ + {text: '十', reading: 'じゅう'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['鏡ノ間', 'かがみのま'], + [ + {text: '鏡', reading: 'かがみ'}, + {text: 'ノ', reading: 'の'}, + {text: '間', reading: 'ま'} + ] + ], + [ + ['鏡ノ間', 'かがみのま'], + [ + {text: '鏡', reading: 'かがみ'}, + {text: 'ノ', reading: 'の'}, + {text: '間', reading: 'ま'} + ] + ], + [ + ['ページ違反', 'ぺーじいはん'], + [ + {text: 'ペ', reading: 'ぺ'}, + {text: 'ー', reading: ''}, + {text: 'ジ', reading: 'じ'}, + {text: '違反', reading: 'いはん'} + ] + ], + // Mismatched kana + [ + ['サボる', 'サボル'], + [ + {text: 'サボ', reading: ''}, + {text: 'る', reading: 'ル'} + ] + ], + // Reading starts with term, but has remainder characters + [ + ['シック', 'シック・ビルしょうこうぐん'], + [ + {text: 'シック', reading: 'シック・ビルしょうこうぐん'} + ] + ], + // Kanji distribution tests + [ + ['逸らす', 'そらす'], + [ + {text: '逸', reading: 'そ'}, + {text: 'らす', reading: ''} + ] + ], + [ + ['逸らす', 'そらす'], + [ + {text: '逸', reading: 'そ'}, + {text: 'らす', reading: ''} + ] + ] + ]; + + for (const [[term, reading], expected] of data) { + const actual = jp.distributeFurigana(term, reading); + vm.assert.deepStrictEqual(actual, expected); + } +} + +/** */ +function testDistributeFuriganaInflected() { + /** @type {[input: [term: string, reading: string, source: string], expected: {text: string, reading: string}[]][]} */ + const data = [ + [ + ['美味しい', 'おいしい', '美味しかた'], + [ + {text: '美味', reading: 'おい'}, + {text: 'しかた', reading: ''} + ] + ], + [ + ['食べる', 'たべる', '食べた'], + [ + {text: '食', reading: 'た'}, + {text: 'べた', reading: ''} + ] + ], + [ + ['迄に', 'までに', 'までに'], + [ + {text: 'までに', reading: ''} + ] + ], + [ + ['行う', 'おこなう', 'おこなわなかった'], + [ + {text: 'おこなわなかった', reading: ''} + ] + ], + [ + ['いい', 'いい', 'イイ'], + [ + {text: 'イイ', reading: ''} + ] + ], + [ + ['否か', 'いなか', '否カ'], + [ + {text: '否', reading: 'いな'}, + {text: 'カ', reading: 'か'} + ] + ] + ]; + + for (const [[term, reading, source], expected] of data) { + const actual = jp.distributeFuriganaInflected(term, reading, source); + vm.assert.deepStrictEqual(actual, expected); + } +} + +/** */ +function testCollapseEmphaticSequences() { + /** @type {[input: [text: string, fullCollapse: boolean], output: [expected: string, expectedSourceMapping: number[]]][]} */ + const data = [ + [['かこい', false], ['かこい', [1, 1, 1]]], + [['かこい', true], ['かこい', [1, 1, 1]]], + [['かっこい', false], ['かっこい', [1, 1, 1, 1]]], + [['かっこい', true], ['かこい', [2, 1, 1]]], + [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]], + [['かっっこい', true], ['かこい', [3, 1, 1]]], + [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]], + [['かっっっこい', true], ['かこい', [4, 1, 1]]], + + [['こい', false], ['こい', [1, 1]]], + [['こい', true], ['こい', [1, 1]]], + [['っこい', false], ['っこい', [1, 1, 1]]], + [['っこい', true], ['こい', [2, 1]]], + [['っっこい', false], ['っこい', [2, 1, 1]]], + [['っっこい', true], ['こい', [3, 1]]], + [['っっっこい', false], ['っこい', [3, 1, 1]]], + [['っっっこい', true], ['こい', [4, 1]]], + + [['すごい', false], ['すごい', [1, 1, 1]]], + [['すごい', true], ['すごい', [1, 1, 1]]], + [['すごーい', false], ['すごーい', [1, 1, 1, 1]]], + [['すごーい', true], ['すごい', [1, 2, 1]]], + [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]], + [['すごーーい', true], ['すごい', [1, 3, 1]]], + [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]], + [['すっごーい', true], ['すごい', [2, 2, 1]]], + [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]], + [['すっっごーーい', true], ['すごい', [3, 3, 1]]], + + [['', false], ['', []]], + [['', true], ['', []]], + [['っ', false], ['っ', [1]]], + [['っ', true], ['', [1]]], + [['っっ', false], ['っ', [2]]], + [['っっ', true], ['', [2]]], + [['っっっ', false], ['っ', [3]]], + [['っっっ', true], ['', [3]]] + ]; + + for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) { + const sourceMap = new TextSourceMap2(text); + const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null); + const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap); + assert.strictEqual(actual1, expected); + assert.strictEqual(actual2, expected); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap2(text, expectedSourceMapping))); + } + } +} + +/** */ +function testIsMoraPitchHigh() { + /** @type {[input: [moraIndex: number, pitchAccentDownstepPosition: number], expected: boolean][]} */ + const data = [ + [[0, 0], false], + [[1, 0], true], + [[2, 0], true], + [[3, 0], true], + + [[0, 1], true], + [[1, 1], false], + [[2, 1], false], + [[3, 1], false], + + [[0, 2], false], + [[1, 2], true], + [[2, 2], false], + [[3, 2], false], + + [[0, 3], false], + [[1, 3], true], + [[2, 3], true], + [[3, 3], false], + + [[0, 4], false], + [[1, 4], true], + [[2, 4], true], + [[3, 4], true] + ]; + + for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) { + const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition); + assert.strictEqual(actual, expected); + } +} + +/** */ +function testGetKanaMorae() { + /** @type {[text: string, expected: string[]][]} */ + const data = [ + ['かこ', ['か', 'こ']], + ['かっこ', ['か', 'っ', 'こ']], + ['カコ', ['カ', 'コ']], + ['カッコ', ['カ', 'ッ', 'コ']], + ['コート', ['コ', 'ー', 'ト']], + ['ちゃんと', ['ちゃ', 'ん', 'と']], + ['とうきょう', ['と', 'う', 'きょ', 'う']], + ['ぎゅう', ['ぎゅ', 'う']], + ['ディスコ', ['ディ', 'ス', 'コ']] + ]; + + for (const [text, expected] of data) { + const actual = jp.getKanaMorae(text); + vm.assert.deepStrictEqual(actual, expected); + } +} + + +/** */ +function main() { + testIsCodePointKanji(); + testIsCodePointKana(); + testIsCodePointJapanese(); + testIsStringEntirelyKana(); + testIsStringPartiallyJapanese(); + testConvertKatakanaToHiragana(); + testConvertHiraganaToKatakana(); + testConvertToRomaji(); + testConvertNumericToFullWidth(); + testConvertHalfWidthKanaToFullWidth(); + testConvertAlphabeticToKana(); + testDistributeFurigana(); + testDistributeFuriganaInflected(); + testCollapseEmphaticSequences(); + testIsMoraPitchHigh(); + testGetKanaMorae(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-json-schema.js b/test/test-json-schema.js new file mode 100644 index 00000000..f9cb023c --- /dev/null +++ b/test/test-json-schema.js @@ -0,0 +1,1048 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM(); +vm.execute([ + 'js/core.js', + 'js/core/extension-error.js', + 'js/general/cache-map.js', + 'js/data/json-schema.js' +]); +/** @type {typeof JsonSchema} */ +const JsonSchema2 = vm.getSingle('JsonSchema'); + + +/** + * @param {import('json-schema').Schema} schema + * @param {unknown} value + * @returns {boolean} + */ +function schemaValidate(schema, value) { + return new JsonSchema2(schema).isValid(value); +} + +/** + * @param {import('json-schema').Schema} schema + * @param {unknown} value + * @returns {import('json-schema').Value} + */ +function getValidValueOrDefault(schema, value) { + return new JsonSchema2(schema).getValidValueOrDefault(value); +} + +/** + * @param {import('json-schema').Schema} schema + * @param {import('json-schema').Value} value + * @returns {import('json-schema').Value} + */ +function createProxy(schema, value) { + return new JsonSchema2(schema).createProxy(value); +} + +/** + * @template T + * @param {T} value + * @returns {T} + */ +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + + +/** */ +function testValidate1() { + /** @type {import('json-schema').Schema} */ + const schema = { + allOf: [ + { + type: 'number' + }, + { + anyOf: [ + {minimum: 10, maximum: 100}, + {minimum: -100, maximum: -10} + ] + }, + { + oneOf: [ + {multipleOf: 3}, + {multipleOf: 5} + ] + }, + { + not: { + anyOf: [ + {multipleOf: 20} + ] + } + } + ] + }; + + /** + * @param {number} value + * @returns {boolean} + */ + const jsValidate = (value) => { + return ( + typeof value === 'number' && + ( + (value >= 10 && value <= 100) || + (value >= -100 && value <= -10) + ) && + ( + ( + (value % 3) === 0 || + (value % 5) === 0 + ) && + (value % 15) !== 0 + ) && + (value % 20) !== 0 + ); + }; + + for (let i = -111; i <= 111; i++) { + const actual = schemaValidate(schema, i); + const expected = jsValidate(i); + assert.strictEqual(actual, expected, `i=${i}; expected=${expected}; actual=${actual}`); + } +} + +/** */ +function testValidate2() { + /** @type {{schema: import('json-schema').Schema, inputs: {expected: boolean, value: unknown}[]}[]} */ + const data = [ + // String tests + { + schema: { + type: 'string' + }, + inputs: [ + {expected: false, value: null}, + {expected: false, value: void 0}, + {expected: false, value: 0}, + {expected: false, value: {}}, + {expected: false, value: []}, + {expected: true, value: ''} + ] + }, + { + schema: { + type: 'string', + minLength: 2 + }, + inputs: [ + {expected: false, value: ''}, + {expected: false, value: '1'}, + {expected: true, value: '12'}, + {expected: true, value: '123'} + ] + }, + { + schema: { + type: 'string', + maxLength: 2 + }, + inputs: [ + {expected: true, value: ''}, + {expected: true, value: '1'}, + {expected: true, value: '12'}, + {expected: false, value: '123'} + ] + }, + { + schema: { + type: 'string', + pattern: 'test' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: false, value: 'TEST'}, + {expected: true, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '^test$' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: false, value: 'TEST'}, + {expected: false, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '^test$', + patternFlags: 'i' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: true, value: 'TEST'}, + {expected: false, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '*' + }, + inputs: [ + {expected: false, value: ''} + ] + }, + { + schema: { + type: 'string', + pattern: '.', + patternFlags: '?' + }, + inputs: [ + {expected: false, value: ''} + ] + }, + + // Const tests + { + schema: { + const: 32 + }, + inputs: [ + {expected: true, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: '32' + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: true, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: null + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: true, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: {a: 'b'} + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: [1, 2, 3] + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + + // Array contains tests + { + schema: { + type: 'array', + contains: {const: 32} + }, + inputs: [ + {expected: false, value: []}, + {expected: true, value: [32]}, + {expected: true, value: [1, 32]}, + {expected: true, value: [1, 32, 1]}, + {expected: false, value: [33]}, + {expected: false, value: [1, 33]}, + {expected: false, value: [1, 33, 1]} + ] + }, + + // Number limits tests + { + schema: { + type: 'number', + minimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'number', + exclusiveMinimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: false, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'number', + maximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'number', + exclusiveMaximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: false, value: 0}, + {expected: false, value: 1} + ] + }, + + // Integer limits tests + { + schema: { + type: 'integer', + minimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'integer', + exclusiveMinimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: false, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'integer', + maximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'integer', + exclusiveMaximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: false, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'integer', + multipleOf: 2 + }, + inputs: [ + {expected: true, value: -2}, + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1}, + {expected: true, value: 2} + ] + }, + + // Numeric type tests + { + schema: { + type: 'number' + }, + inputs: [ + {expected: true, value: 0}, + {expected: true, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + type: 'integer' + }, + inputs: [ + {expected: true, value: 0}, + {expected: false, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + + // Reference tests + { + schema: { + definitions: { + example: { + type: 'number' + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: true, value: 0}, + {expected: true, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + definitions: { + example: { + type: 'integer' + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: true, value: 0}, + {expected: false, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: false, value: 0}, + {expected: false, value: 0.5}, + {expected: false, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: true, value: {}}, + {expected: false, value: {test: 0}}, + {expected: false, value: {test: 0.5}}, + {expected: false, value: {test: 1}}, + {expected: false, value: {test: '0'}}, + {expected: false, value: {test: null}}, + {expected: false, value: {test: []}}, + {expected: true, value: {test: {}}}, + {expected: true, value: {test: {test: {}}}}, + {expected: true, value: {test: {test: {test: {}}}}} + ] + } + ]; + + for (const {schema, inputs} of data) { + for (const {expected, value} of inputs) { + const actual = schemaValidate(schema, value); + assert.strictEqual(actual, expected); + } + } +} + + +/** */ +function testGetValidValueOrDefault1() { + /** @type {{schema: import('json-schema').Schema, inputs: [value: unknown, expected: unknown][]}[]} */ + const data = [ + // Test value defaulting on objects with additionalProperties=false + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: false + }, + inputs: [ + [ + void 0, + {test: 'default'} + ], + [ + null, + {test: 'default'} + ], + [ + 0, + {test: 'default'} + ], + [ + '', + {test: 'default'} + ], + [ + [], + {test: 'default'} + ], + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default'} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value'} + ] + ] + }, + + // Test value defaulting on objects with additionalProperties=true + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: true + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default', test2: 'value2'} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value', test2: 'value2'} + ] + ] + }, + + // Test value defaulting on objects with additionalProperties={schema} + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: { + type: 'number', + default: 10 + } + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default', test2: 10} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value', test2: 10} + ], + [ + {test2: 2}, + {test: 'default', test2: 2} + ], + [ + {test: 'value', test2: 2}, + {test: 'value', test2: 2} + ], + [ + {test: 'value', test2: 2, test3: null}, + {test: 'value', test2: 2, test3: 10} + ], + [ + {test: 'value', test2: 2, test3: void 0}, + {test: 'value', test2: 2, test3: 10} + ] + ] + }, + + // Test value defaulting where hasOwnProperty is false + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + Object.create({test: 'value'}), + {test: 'default'} + ] + ] + }, + { + schema: { + type: 'object', + required: ['toString'], + properties: { + toString: /** @type {import('json-schema').SchemaObject} */ ({ + type: 'string', + default: 'default' + }) + } + }, + inputs: [ + [ + {}, + {toString: 'default'} + ], + [ + {toString: 'value'}, + {toString: 'value'} + ], + [ + Object.create({toString: 'value'}), + {toString: 'default'} + ] + ] + }, + + // Test enum + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'value1', + enum: ['value1', 'value2', 'value3'] + } + } + }, + inputs: [ + [ + {test: 'value1'}, + {test: 'value1'} + ], + [ + {test: 'value2'}, + {test: 'value2'} + ], + [ + {test: 'value3'}, + {test: 'value3'} + ], + [ + {test: 'value4'}, + {test: 'value1'} + ] + ] + }, + + // Test valid vs invalid default + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'integer', + default: 2, + minimum: 1 + } + } + }, + inputs: [ + [ + {test: -1}, + {test: 2} + ] + ] + }, + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'integer', + default: 1, + minimum: 2 + } + } + }, + inputs: [ + [ + {test: -1}, + {test: -1} + ] + ] + }, + + // Test references + { + schema: { + definitions: { + example: { + type: 'number', + default: 0 + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + [ + 1, + 1 + ], + [ + null, + 0 + ], + [ + 'test', + 0 + ], + [ + {test: 'value'}, + 0 + ] + ] + }, + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + [ + 1, + {} + ], + [ + null, + {} + ], + [ + 'test', + {} + ], + [ + {}, + {} + ], + [ + {test: {}}, + {test: {}} + ], + [ + {test: 'value'}, + {test: {}} + ], + [ + {test: {test: {}}}, + {test: {test: {}}} + ] + ] + } + ]; + + for (const {schema, inputs} of data) { + for (const [value, expected] of inputs) { + const actual = getValidValueOrDefault(schema, value); + vm.assert.deepStrictEqual(actual, expected); + } + } +} + + +/** */ +function testProxy1() { + /** @type {{schema: import('json-schema').Schema, tests: {error: boolean, value?: import('json-schema').Value, action: (value: import('core').SafeAny) => void}[]}[]} */ + const data = [ + // Object tests + { + schema: { + type: 'object', + required: ['test'], + additionalProperties: false, + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + tests: [ + {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, + {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, + {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} + ] + }, + { + schema: { + type: 'object', + required: ['test'], + additionalProperties: true, + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + tests: [ + {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, + {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, + {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, + {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} + ] + }, + { + schema: { + type: 'object', + required: ['test1'], + additionalProperties: false, + properties: { + test1: { + type: 'object', + required: ['test2'], + additionalProperties: false, + properties: { + test2: { + type: 'object', + required: ['test3'], + additionalProperties: false, + properties: { + test3: { + type: 'string', + default: 'default' + } + } + } + } + } + } + }, + tests: [ + {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }}, + {error: true, action: (value) => { value.test1.test2.test3 = null; }}, + {error: true, action: (value) => { delete value.test1.test2.test3; }}, + {error: true, action: (value) => { value.test1.test2 = null; }}, + {error: true, action: (value) => { value.test1 = null; }}, + {error: true, action: (value) => { value.test4 = 'string'; }}, + {error: false, action: (value) => { delete value.test4; }} + ] + }, + + // Array tests + { + schema: { + type: 'array', + items: { + type: 'string', + default: 'default' + } + }, + tests: [ + {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }}, + {error: true, value: ['default'], action: (value) => { value[0] = null; }}, + {error: false, value: ['default'], action: (value) => { delete value[0]; }}, + {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }}, + {error: false, value: ['default'], action: (value) => { + value[1] = 'string'; + if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); } + if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); } + }} + ] + }, + + // Reference tests + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + tests: [ + {error: false, value: {}, action: (value) => { value.test = {}; }}, + {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }}, + {error: false, value: {}, action: (value) => { value.test = {test: {}}; }}, + {error: true, value: {}, action: (value) => { value.test = null; }}, + {error: true, value: {}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }}, + {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }} + ] + } + ]; + + for (const {schema, tests} of data) { + for (let {error, value, action} of tests) { + if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); } + value = clone(value); + assert.ok(schemaValidate(schema, value)); + const valueProxy = createProxy(schema, value); + if (error) { + assert.throws(() => action(valueProxy)); + } else { + assert.doesNotThrow(() => action(valueProxy)); + } + } + } +} + + +/** */ +function main() { + testValidate1(); + testValidate2(); + testGetValidValueOrDefault1(); + testProxy1(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-manifest.js b/test/test-manifest.js new file mode 100644 index 00000000..32a498e1 --- /dev/null +++ b/test/test-manifest.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {ManifestUtil} = require('../dev/manifest-util'); + + +/** + * @returns {string} + */ +function loadManifestString() { + const manifestPath = path.join(__dirname, '..', 'ext', 'manifest.json'); + return fs.readFileSync(manifestPath, {encoding: 'utf8'}); +} + +/** */ +function validateManifest() { + const manifestUtil = new ManifestUtil(); + const manifest1 = loadManifestString(); + const manifest2 = ManifestUtil.createManifestString(manifestUtil.getManifest()); + assert.strictEqual(manifest1, manifest2, 'Manifest data does not match.'); +} + + +/** */ +function main() { + validateManifest(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js new file mode 100644 index 00000000..8826d6a9 --- /dev/null +++ b/test/test-object-property-accessor.js @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM({}); +vm.execute('js/general/object-property-accessor.js'); +/** @type {typeof ObjectPropertyAccessor} */ +const ObjectPropertyAccessor2 = vm.getSingle('ObjectPropertyAccessor'); + + +/** + * @returns {import('core').UnknownObject} + */ +function createTestObject() { + return { + 0: null, + value1: { + value2: {}, + value3: [], + value4: null + }, + value5: [ + {}, + [], + null + ] + }; +} + + +/** */ +function testGet1() { + /** @type {[pathArray: (string|number)[], getExpected: (object: import('core').SafeAny) => unknown][]} */ + const data = [ + [[], (object) => object], + [['0'], (object) => object['0']], + [['value1'], (object) => object.value1], + [['value1', 'value2'], (object) => object.value1.value2], + [['value1', 'value3'], (object) => object.value1.value3], + [['value1', 'value4'], (object) => object.value1.value4], + [['value5'], (object) => object.value5], + [['value5', 0], (object) => object.value5[0]], + [['value5', 1], (object) => object.value5[1]], + [['value5', 2], (object) => object.value5[2]] + ]; + + for (const [pathArray, getExpected] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + const expected = getExpected(object); + + assert.strictEqual(accessor.get(pathArray), expected); + } +} + +/** */ +function testGet2() { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + /** @type {[pathArray: (string|number)[], message: string][]} */ + const data = [ + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['invalid'], 'Invalid path: invalid'], + [['value1', 'invalid'], 'Invalid path: value1.invalid'], + [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 'length'], 'Invalid path: value5.length'], + [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'], + [['value5', 0, 0], 'Invalid path: value5[0][0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 1, 0], 'Invalid path: value5[1][0]'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'] + ]; + + for (const [pathArray, message] of data) { + assert.throws(() => accessor.get(pathArray), {message}); + } +} + + +/** */ +function testSet1() { + const testValue = {}; + /** @type {(string|number)[][]} */ + const data = [ + ['0'], + ['value1', 'value2'], + ['value1', 'value3'], + ['value1', 'value4'], + ['value1'], + ['value5', 0], + ['value5', 1], + ['value5', 2], + ['value5'] + ]; + + for (const pathArray of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + accessor.set(pathArray, testValue); + assert.strictEqual(accessor.get(pathArray), testValue); + } +} + +/** */ +function testSet2() { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + const testValue = {}; + /** @type {[pathArray: (string|number)[], message: string][]} */ + const data = [ + [[], 'Invalid path'], + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'] + ]; + + for (const [pathArray, message] of data) { + assert.throws(() => accessor.set(pathArray, testValue), {message}); + } +} + + +/** */ +function testDelete1() { + /** + * @param {unknown} object + * @param {string} property + * @returns {boolean} + */ + const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property); + + /** @type {[pathArray: (string|number)[], validate: (object: import('core').SafeAny) => boolean][]} */ + const data = [ + [['0'], (object) => !hasOwn(object, '0')], + [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')], + [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')], + [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')], + [['value1'], (object) => !hasOwn(object, 'value1')], + [['value5'], (object) => !hasOwn(object, 'value5')] + ]; + + for (const [pathArray, validate] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + accessor.delete(pathArray); + assert.ok(validate(object)); + } +} + +/** */ +function testDelete2() { + /** @type {[pathArray: (string|number)[], message: string][]} */ + const data = [ + [[], 'Invalid path'], + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'], + [['value5', 0], 'Invalid type'], + [['value5', 1], 'Invalid type'], + [['value5', 2], 'Invalid type'] + ]; + + for (const [pathArray, message] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + assert.throws(() => accessor.delete(pathArray), {message}); + } +} + + +/** */ +function testSwap1() { + /** @type {[pathArray: (string|number)[], compareValues: boolean][]} */ + const data = [ + [['0'], true], + [['value1', 'value2'], true], + [['value1', 'value3'], true], + [['value1', 'value4'], true], + [['value1'], false], + [['value5', 0], true], + [['value5', 1], true], + [['value5', 2], true], + [['value5'], false] + ]; + + for (const [pathArray1, compareValues1] of data) { + for (const [pathArray2, compareValues2] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + const value1a = accessor.get(pathArray1); + const value2a = accessor.get(pathArray2); + + accessor.swap(pathArray1, pathArray2); + + if (!compareValues1 || !compareValues2) { continue; } + + const value1b = accessor.get(pathArray1); + const value2b = accessor.get(pathArray2); + + assert.deepStrictEqual(value1a, value2b); + assert.deepStrictEqual(value2a, value1b); + } + } +} + +/** */ +function testSwap2() { + /** @type {[pathArray1: (string|number)[], pathArray2: (string|number)[], checkRevert: boolean, message: string][]} */ + const data = [ + [[], [], false, 'Invalid path 1'], + [['0'], [], false, 'Invalid path 2'], + [[], ['0'], false, 'Invalid path 1'], + [[0], ['0'], false, 'Invalid path 1: [0]'], + [['0'], [0], false, 'Invalid path 2: [0]'] + ]; + + for (const [pathArray1, pathArray2, checkRevert, message] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor2(object); + + let value1a; + let value2a; + if (checkRevert) { + try { + value1a = accessor.get(pathArray1); + value2a = accessor.get(pathArray2); + } catch (e) { + // NOP + } + } + + assert.throws(() => accessor.swap(pathArray1, pathArray2), {message}); + + if (!checkRevert) { continue; } + + const value1b = accessor.get(pathArray1); + const value2b = accessor.get(pathArray2); + + assert.deepStrictEqual(value1a, value1b); + assert.deepStrictEqual(value2a, value2b); + } +} + + +/** */ +function testGetPathString1() { + /** @type {[pathArray: (string|number)[], expected: string][]} */ + const data = [ + [[], ''], + [[0], '[0]'], + [['escape\\'], '["escape\\\\"]'], + [['\'quote\''], '["\'quote\'"]'], + [['"quote"'], '["\\"quote\\""]'], + [['part1', 'part2'], 'part1.part2'], + [['part1', 'part2', 3], 'part1.part2[3]'], + [['part1', 'part2', '3'], 'part1.part2["3"]'], + [['part1', 'part2', '3part'], 'part1.part2["3part"]'], + [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'], + [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]'] + ]; + + for (const [pathArray, expected] of data) { + assert.strictEqual(ObjectPropertyAccessor2.getPathString(pathArray), expected); + } +} + +/** */ +function testGetPathString2() { + /** @type {[pathArray: unknown[], message: string][]} */ + const data = [ + [[1.5], 'Invalid index'], + [[null], 'Invalid type: object'] + ]; + + for (const [pathArray, message] of data) { + // @ts-ignore - Throwing is expected + assert.throws(() => ObjectPropertyAccessor2.getPathString(pathArray), {message}); + } +} + + +/** */ +function testGetPathArray1() { + /** @type {[pathString: string, pathArray: (string|number)[]][]} */ + const data = [ + ['', []], + ['[0]', [0]], + ['["escape\\\\"]', ['escape\\']], + ['["\'quote\'"]', ['\'quote\'']], + ['["\\"quote\\""]', ['"quote"']], + ['part1.part2', ['part1', 'part2']], + ['part1.part2[3]', ['part1', 'part2', 3]], + ['part1.part2["3"]', ['part1', 'part2', '3']], + ['part1.part2[\'3\']', ['part1', 'part2', '3']], + ['part1.part2["3part"]', ['part1', 'part2', '3part']], + ['part1.part2[\'3part\']', ['part1', 'part2', '3part']], + ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']], + ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']], + ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']], + ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']] + ]; + + for (const [pathString, expected] of data) { + // @ts-ignore + vm.assert.deepStrictEqual(ObjectPropertyAccessor2.getPathArray(pathString), expected); + } +} + +/** */ +function testGetPathArray2() { + /** @type {[pathString: string, message: string][]} */ + const data = [ + ['?', 'Unexpected character: ?'], + ['.', 'Unexpected character: .'], + ['0', 'Unexpected character: 0'], + ['part1.[0]', 'Unexpected character: ['], + ['part1?', 'Unexpected character: ?'], + ['[part1]', 'Unexpected character: p'], + ['[0a]', 'Unexpected character: a'], + ['["part1"x]', 'Unexpected character: x'], + ['[\'part1\'x]', 'Unexpected character: x'], + ['["part1"]x', 'Unexpected character: x'], + ['[\'part1\']x', 'Unexpected character: x'], + ['part1..part2', 'Unexpected character: .'], + + ['[', 'Path not terminated correctly'], + ['part1.', 'Path not terminated correctly'], + ['part1[', 'Path not terminated correctly'], + ['part1["', 'Path not terminated correctly'], + ['part1[\'', 'Path not terminated correctly'], + ['part1[""', 'Path not terminated correctly'], + ['part1[\'\'', 'Path not terminated correctly'], + ['part1[0', 'Path not terminated correctly'], + ['part1[0].', 'Path not terminated correctly'] + ]; + + for (const [pathString, message] of data) { + assert.throws(() => ObjectPropertyAccessor2.getPathArray(pathString), {message}); + } +} + + +/** */ +function testHasProperty() { + /** @type {[object: unknown, property: unknown, expected: boolean][]} */ + const data = [ + [{}, 'invalid', false], + [{}, 0, false], + [{valid: 0}, 'valid', true], + [{null: 0}, null, false], + [[], 'invalid', false], + [[], 0, false], + [[0], 0, true], + [[0], null, false], + ['string', 0, false], + ['string', 'length', false], + ['string', null, false] + ]; + + for (const [object, property, expected] of data) { + // @ts-ignore + assert.strictEqual(ObjectPropertyAccessor2.hasProperty(object, property), expected); + } +} + +/** */ +function testIsValidPropertyType() { + /** @type {[object: unknown, property: unknown, expected: boolean][]} */ + const data = [ + [{}, 'invalid', true], + [{}, 0, false], + [{valid: 0}, 'valid', true], + [{null: 0}, null, false], + [[], 'invalid', false], + [[], 0, true], + [[0], 0, true], + [[0], null, false], + ['string', 0, false], + ['string', 'length', false], + ['string', null, false] + ]; + + for (const [object, property, expected] of data) { + // @ts-ignore + assert.strictEqual(ObjectPropertyAccessor2.isValidPropertyType(object, property), expected); + } +} + + +/** */ +function main() { + testGet1(); + testGet2(); + testSet1(); + testSet2(); + testDelete1(); + testDelete2(); + testSwap1(); + testSwap2(); + testGetPathString1(); + testGetPathString2(); + testGetPathArray1(); + testGetPathArray2(); + testHasProperty(); + testIsValidPropertyType(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-profile-conditions-util.js b/test/test-profile-conditions-util.js new file mode 100644 index 00000000..2e6f751f --- /dev/null +++ b/test/test-profile-conditions-util.js @@ -0,0 +1,1136 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + + +const vm = new VM({}); +vm.execute([ + 'js/core.js', + 'js/core/extension-error.js', + 'js/general/cache-map.js', + 'js/data/json-schema.js', + 'js/background/profile-conditions-util.js' +]); +/** @type {typeof ProfileConditionsUtil} */ +const ProfileConditionsUtil2 = vm.getSingle('ProfileConditionsUtil'); + + +/** */ +function testNormalizeContext() { + /** @type {{context: import('settings').OptionsContext, expected: import('profile-conditions-util').NormalizedOptionsContext}[]} */ + const data = [ + // Empty + { + context: {index: 0}, + expected: {index: 0, flags: []} + }, + + // Domain normalization + { + context: {depth: 0, url: ''}, + expected: {depth: 0, url: '', flags: []} + }, + { + context: {depth: 0, url: 'http://example.com/'}, + expected: {depth: 0, url: 'http://example.com/', domain: 'example.com', flags: []} + }, + { + context: {depth: 0, url: 'http://example.com:1234/'}, + expected: {depth: 0, url: 'http://example.com:1234/', domain: 'example.com', flags: []} + }, + { + context: {depth: 0, url: 'http://user@example.com:1234/'}, + expected: {depth: 0, url: 'http://user@example.com:1234/', domain: 'example.com', flags: []} + } + ]; + + for (const {context, expected} of data) { + const profileConditionsUtil = new ProfileConditionsUtil2(); + const actual = profileConditionsUtil.normalizeContext(context); + vm.assert.deepStrictEqual(actual, expected); + } +} + +/** */ +function testSchemas() { + /** @type {{conditionGroups: import('settings').ProfileConditionGroup[], expectedSchema?: import('json-schema').Schema, inputs?: {expected: boolean, context: import('settings').OptionsContext}[]}[]} */ + const data = [ + // Empty + { + conditionGroups: [], + expectedSchema: {}, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + {conditions: []} + ], + expectedSchema: {}, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + {conditions: []}, + {conditions: []} + ], + expectedSchema: {}, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}} + ] + }, + + // popupLevel tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: {const: 0} + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'notEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + not: { + anyOf: [ + { + properties: { + depth: {const: 0} + }, + required: ['depth'] + } + ] + } + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThan', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + maximum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + minimum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + + // url tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com' + } + ] + } + ], + expectedSchema: { + properties: { + domain: { + oneOf: [ + {const: 'example.com'} + ] + } + }, + required: ['domain'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com, example1.com, example2.com' + } + ] + } + ], + expectedSchema: { + properties: { + domain: { + oneOf: [ + {const: 'example.com'}, + {const: 'example1.com'}, + {const: 'example2.com'} + ] + } + }, + required: ['domain'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example3.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchRegExp', + value: '^http://example\\d?\\.com/[\\w\\W]*$' + } + ] + } + ], + expectedSchema: { + properties: { + url: { + type: 'string', + pattern: '^http://example\\d?\\.com/[\\w\\W]*$', + patternFlags: 'i' + } + }, + required: ['url'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example3.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/example'}}, + {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}}, + {expected: false, context: {depth: 0, url: 'http://example-1.com/'}} + ] + }, + + // modifierKeys tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + maxItems: 0, + minItems: 0 + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: 'alt, shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'alt'}}, + {contains: {const: 'shift'}} + ] + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'areNot', + value: '' + } + ] + } + ], + expectedSchema: { + not: { + anyOf: [ + { + properties: { + modifierKeys: { + type: 'array', + maxItems: 0, + minItems: 0 + } + }, + required: ['modifierKeys'] + } + ] + } + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'areNot', + value: 'alt, shift' + } + ] + } + ], + expectedSchema: { + not: { + anyOf: [ + { + properties: { + modifierKeys: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'alt'}}, + {contains: {const: 'shift'}} + ] + } + }, + required: ['modifierKeys'] + } + ] + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'include', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + minItems: 0 + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'include', + value: 'alt, shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + minItems: 2, + allOf: [ + {contains: {const: 'alt'}}, + {contains: {const: 'shift'}} + ] + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'notInclude', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array' + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'notInclude', + value: 'alt, shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + not: { + anyOf: [ + {contains: {const: 'alt'}}, + {contains: {const: 'shift'}} + ] + } + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}} + ] + }, + + // flags tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: ''}}, + {expected: true, context: {depth: 0, url: '', flags: []}}, + {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: 'clipboard, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'clipboard'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {depth: 0, url: ''}}, + {expected: false, context: {depth: 0, url: '', flags: []}}, + {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: '' + } + ] + } + ], + expectedSchema: { + not: { + anyOf: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + } + ] + } + }, + inputs: [ + {expected: false, context: {depth: 0, url: ''}}, + {expected: false, context: {depth: 0, url: '', flags: []}}, + {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: 'clipboard, test2' + } + ] + } + ], + expectedSchema: { + not: { + anyOf: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'clipboard'}}, + {contains: {const: 'test2'}} + ] + } + } + } + ] + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: ''}}, + {expected: true, context: {depth: 0, url: '', flags: []}}, + {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: ''}}, + {expected: true, context: {depth: 0, url: '', flags: []}}, + {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: 'clipboard, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 2, + allOf: [ + {contains: {const: 'clipboard'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {depth: 0, url: ''}}, + {expected: false, context: {depth: 0, url: '', flags: []}}, + {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array' + } + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: ''}}, + {expected: true, context: {depth: 0, url: '', flags: []}}, + {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: 'clipboard, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + not: { + anyOf: [ + {contains: {const: 'clipboard'}}, + {contains: {const: 'test2'}} + ] + } + } + } + }, + inputs: [ + {expected: true, context: {depth: 0, url: ''}}, + {expected: true, context: {depth: 0, url: '', flags: []}}, + {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, + // @ts-ignore - Ignore type for string flag for testing purposes + {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} + ] + }, + + // Multiple conditions tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + } + ], + expectedSchema: { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + }, + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: '0' + } + ] + } + ], + expectedSchema: { + anyOf: [ + { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + { + properties: { + depth: {const: 0} + }, + required: ['depth'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + }, + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: '0' + }, + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: '-1' + } + ] + } + ], + expectedSchema: { + anyOf: [ + { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + { + allOf: [ + { + properties: { + depth: { + type: 'number', + maximum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + minimum: -1 + } + }, + required: ['depth'] + } + ] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + } + ]; + + for (const {conditionGroups, expectedSchema, inputs} of data) { + const profileConditionsUtil = new ProfileConditionsUtil2(); + const schema = profileConditionsUtil.createSchema(conditionGroups); + if (typeof expectedSchema !== 'undefined') { + vm.assert.deepStrictEqual(schema.schema, expectedSchema); + } + if (Array.isArray(inputs)) { + for (const {expected, context} of inputs) { + const normalizedContext = profileConditionsUtil.normalizeContext(context); + const actual = schema.isValid(normalizedContext); + assert.strictEqual(actual, expected); + } + } + } +} + + +/** */ +function main() { + testNormalizeContext(); + testSchemas(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-text-source-map.js b/test/test-text-source-map.js new file mode 100644 index 00000000..834a3d07 --- /dev/null +++ b/test/test-text-source-map.js @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + +const vm = new VM(); +vm.execute(['js/general/text-source-map.js']); +/** @type {typeof TextSourceMap} */ +const TextSourceMap2 = vm.getSingle('TextSourceMap'); + + +/** */ +function testSource() { + const data = [ + ['source1'], + ['source2'], + ['source3'] + ]; + + for (const [source] of data) { + const sourceMap = new TextSourceMap2(source); + assert.strictEqual(source, sourceMap.source); + } +} + +/** */ +function testEquals() { + /** @type {[args1: [source1: string, mapping1: ?(number[])], args2: [source2: string, mapping2: ?(number[])], expectedEquals: boolean][]} */ + const data = [ + [['source1', null], ['source1', null], true], + [['source2', null], ['source2', null], true], + [['source3', null], ['source3', null], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true], + + [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true], + [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true], + [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true], + + [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false], + [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false], + [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false] + ]; + + for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) { + const sourceMap1 = new TextSourceMap2(source1, mapping1); + const sourceMap2 = new TextSourceMap2(source2, mapping2); + assert.ok(sourceMap1.equals(sourceMap1)); + assert.ok(sourceMap2.equals(sourceMap2)); + assert.strictEqual(sourceMap1.equals(sourceMap2), expectedEquals); + } +} + +/** */ +function testGetSourceLength() { + /** @type {[args: [source: string, mapping: number[]], finalLength: number, expectedValue: number][]} */ + const data = [ + [['source', [1, 1, 1, 1, 1, 1]], 1, 1], + [['source', [1, 1, 1, 1, 1, 1]], 2, 2], + [['source', [1, 1, 1, 1, 1, 1]], 3, 3], + [['source', [1, 1, 1, 1, 1, 1]], 4, 4], + [['source', [1, 1, 1, 1, 1, 1]], 5, 5], + [['source', [1, 1, 1, 1, 1, 1]], 6, 6], + + [['source', [2, 2, 2]], 1, 2], + [['source', [2, 2, 2]], 2, 4], + [['source', [2, 2, 2]], 3, 6], + + [['source', [3, 3]], 1, 3], + [['source', [3, 3]], 2, 6], + + [['source', [6, 6]], 1, 6] + ]; + + for (const [[source, mapping], finalLength, expectedValue] of data) { + const sourceMap = new TextSourceMap2(source, mapping); + assert.strictEqual(sourceMap.getSourceLength(finalLength), expectedValue); + } +} + +/** */ +function testCombineInsert() { + /** @type {[args: [source: string, mapping: ?(number[])], expectedArgs: [expectedSource: string, expectedMapping: ?(number[])], operations: [operation: string, arg1: number, arg2: number][]][]} */ + const data = [ + // No operations + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1]], + [] + ], + + // Combine + [ + ['source', null], + ['source', [3, 1, 1, 1]], + [ + ['combine', 0, 2] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 3]], + [ + ['combine', 3, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 3, 2], + ['combine', 0, 2] + ] + ], + + // Insert + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1]], + [ + ['insert', 0, 0] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 0, 0], + ['insert', 7, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0], + ['insert', 0, 0] + ] + ], + + // Mixed + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['insert', 1, 0], + ['combine', 2, 2] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2], + ['insert', 1, 0] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['insert', 3, 0], + ['combine', 0, 2], + ['combine', 2, 2] + ] + ] + ]; + + for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) { + const sourceMap = new TextSourceMap2(source, mapping); + const expectedSourceMap = new TextSourceMap2(expectedSource, expectedMapping); + for (const [operation, ...args] of operations) { + switch (operation) { + case 'combine': + sourceMap.combine(...args); + break; + case 'insert': + sourceMap.insert(...args); + break; + } + } + assert.ok(sourceMap.equals(expectedSourceMap)); + } +} + + +/** */ +function main() { + testSource(); + testEquals(); + testGetSourceLength(); + testCombineInsert(); +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-translator.js b/test/test-translator.js new file mode 100644 index 00000000..0c84e0be --- /dev/null +++ b/test/test-translator.js @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {TranslatorVM} = require('../dev/translator-vm'); + + +/** + * @template T + * @param {T} value + * @returns {T} + */ +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + + +/** */ +async function main() { + const write = (process.argv[2] === '--write'); + + const translatorVM = new TranslatorVM(); + const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1'); + await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2'); + + const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json'); + const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); + + const testResults1FilePath = path.join(__dirname, 'data', 'translator-test-results.json'); + const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); + const actualResults1 = []; + + const testResults2FilePath = path.join(__dirname, 'data', 'translator-test-results-note-data1.json'); + const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'})); + const actualResults2 = []; + + for (let i = 0, ii = tests.length; i < ii; ++i) { + const test = tests[i]; + const expected1 = expectedResults1[i]; + const expected2 = expectedResults2[i]; + switch (test.func) { + case 'findTerms': + { + const {name, mode, text} = test; + /** @type {import('translation').FindTermsOptions} */ + const options = translatorVM.buildOptions(optionsPresets, test.options); + const {dictionaryEntries, originalTextLength} = clone(await translatorVM.translator.findTerms(mode, text, options)); + const noteDataList = mode !== 'simple' ? clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), mode))) : null; + actualResults1.push({name, originalTextLength, dictionaryEntries}); + actualResults2.push({name, noteDataList}); + if (!write) { + assert.deepStrictEqual(originalTextLength, expected1.originalTextLength); + assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries); + assert.deepStrictEqual(noteDataList, expected2.noteDataList); + } + } + break; + case 'findKanji': + { + const {name, text} = test; + /** @type {import('translation').FindKanjiOptions} */ + const options = translatorVM.buildOptions(optionsPresets, test.options); + const dictionaryEntries = clone(await translatorVM.translator.findKanji(text, options)); + const noteDataList = clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), 'split'))); + actualResults1.push({name, dictionaryEntries}); + actualResults2.push({name, noteDataList}); + if (!write) { + assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries); + assert.deepStrictEqual(noteDataList, expected2.noteDataList); + } + } + break; + } + } + + if (write) { + // Use 2 indent instead of 4 to save a bit of file size + fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'}); + fs.writeFileSync(testResults2FilePath, JSON.stringify(actualResults2, null, 2), {encoding: 'utf8'}); + } +} + + +if (require.main === module) { testMain(main); } diff --git a/test/test-workers.js b/test/test-workers.js new file mode 100644 index 00000000..3de7ac48 --- /dev/null +++ b/test/test-workers.js @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const {JSDOM} = require('jsdom'); +const {VM} = require('../dev/vm'); +const assert = require('assert'); + + +class StubClass { + /** */ + prepare() { + // NOP + } +} + + +/** + * @returns {import('core').SafeAny} + */ +function loadEslint() { + return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.eslintrc.json'), {encoding: 'utf8'})); +} + +/** + * @param {string[]} scriptPaths + * @returns {string[]} + */ +function filterScriptPaths(scriptPaths) { + const extDirName = 'ext'; + return scriptPaths.filter((src) => !src.startsWith('/lib/')).map((src) => `${extDirName}${src}`); +} + +/** + * @param {string} fileName + * @returns {string[]} + */ +function getAllHtmlScriptPaths(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + const dom = new JSDOM(domSource); + const {window} = dom; + const {document} = window; + try { + const scripts = document.querySelectorAll('script'); + return [...scripts].map(({src}) => src); + } finally { + window.close(); + } +} + +/** + * @param {string[]} scripts + */ +function convertBackgroundScriptsToServiceWorkerScripts(scripts) { + // Use parse5-based SimpleDOMParser + scripts.splice(0, 0, '/lib/parse5.js'); + const index = scripts.indexOf('/js/dom/native-simple-dom-parser.js'); + assert.ok(index >= 0); + scripts[index] = '/js/dom/simple-dom-parser.js'; +} + +/** + * @param {string} scriptPath + * @param {import('core').UnknownObject} fields + * @returns {string[]} + */ +function getImportedScripts(scriptPath, fields) { + /** @type {string[]} */ + const importedScripts = []; + + /** + * @param {...string} scripts + */ + const importScripts = (...scripts) => { + importedScripts.push(...scripts); + }; + + const vm = new VM(Object.assign({importScripts}, fields)); + vm.context.self = vm.context; + vm.execute([scriptPath]); + + return importedScripts; +} + +/** */ +function testServiceWorker() { + // Verify that sw.js scripts match background.html scripts + const extDir = path.join(__dirname, '..', 'ext'); + const scripts = getAllHtmlScriptPaths(path.join(extDir, 'background.html')); + convertBackgroundScriptsToServiceWorkerScripts(scripts); + const importedScripts = getImportedScripts('sw.js', {}); + assert.deepStrictEqual(scripts, importedScripts); + + // Verify that eslint config lists files correctly + const expectedSwRulesFiles = filterScriptPaths(scripts); + const eslintConfig = loadEslint(); + const swRules = /** @type {import('core').SafeAny[]} */ (eslintConfig.overrides).find((item) => ( + typeof item.env === 'object' && + item.env !== null && + item.env.serviceworker === true + )); + assert.ok(typeof swRules !== 'undefined'); + assert.ok(Array.isArray(swRules.files)); + assert.deepStrictEqual(swRules.files, expectedSwRulesFiles); +} + +/** */ +function testWorkers() { + testWorker( + 'js/language/dictionary-worker-main.js', + {DictionaryWorkerHandler: StubClass} + ); +} + +/** + * @param {string} scriptPath + * @param {import('core').UnknownObject} fields + */ +function testWorker(scriptPath, fields) { + // Get script paths + const scripts = getImportedScripts(scriptPath, fields); + + // Verify that eslint config lists files correctly + const expectedRulesFiles = filterScriptPaths(scripts); + const expectedRulesFilesSet = new Set(expectedRulesFiles); + const eslintConfig = loadEslint(); + const rules = /** @type {import('core').SafeAny[]} */ (eslintConfig.overrides).find((item) => ( + typeof item.env === 'object' && + item.env !== null && + item.env.worker === true + )); + assert.ok(typeof rules !== 'undefined'); + assert.ok(Array.isArray(rules.files)); + assert.deepStrictEqual(/** @type {import('core').SafeAny[]} */ (rules.files).filter((v) => expectedRulesFilesSet.has(v)), expectedRulesFiles); +} + + +/** */ +function main() { + try { + testServiceWorker(); + testWorkers(); + } catch (e) { + console.error(e); + process.exit(-1); + return; + } + process.exit(0); +} + + +if (require.main === module) { main(); } |