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