diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/data/dictionaries/valid-dictionary1/image.gif | bin | 0 -> 45 bytes | |||
-rw-r--r-- | test/data/dictionaries/valid-dictionary1/term_bank_1.json | 3 | ||||
-rw-r--r-- | test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json | 6 | ||||
-rw-r--r-- | test/data/html/test-document1.html | 1 | ||||
-rw-r--r-- | test/data/html/test-document2-frame1.html | 6 | ||||
-rw-r--r-- | test/data/html/test-document2-script.js | 24 | ||||
-rw-r--r-- | test/data/html/test-document2.html | 97 | ||||
-rw-r--r-- | test/data/html/test-document3-frame1.html | 44 | ||||
-rw-r--r-- | test/data/html/test-document3-frame2.html | 62 | ||||
-rw-r--r-- | test/data/html/test-document3.html | 26 | ||||
-rw-r--r-- | test/data/html/test-dom-text-scanner.html | 393 | ||||
-rw-r--r-- | test/data/html/test-stylesheet.css | 15 | ||||
-rw-r--r-- | test/test-database.js | 87 | ||||
-rw-r--r-- | test/test-dom-text-scanner.js | 183 | ||||
-rw-r--r-- | test/test-japanese.js | 9 | ||||
-rw-r--r-- | test/test-object-property-accessor.js | 186 | ||||
-rw-r--r-- | test/yomichan-test.js | 15 |
17 files changed, 1045 insertions, 112 deletions
diff --git a/test/data/dictionaries/valid-dictionary1/image.gif b/test/data/dictionaries/valid-dictionary1/image.gif Binary files differnew file mode 100644 index 00000000..f089d07c --- /dev/null +++ b/test/data/dictionaries/valid-dictionary1/image.gif diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json index 755d9f6a..a62ad117 100644 --- a/test/data/dictionaries/valid-dictionary1/term_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json @@ -30,5 +30,6 @@ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 29, ["definition1a (打ち込む, ぶちこむ)", "definition1b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"], ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 30, ["definition2a (打ち込む, ぶちこむ)", "definition2b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"], ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 31, ["definition3a (打ち込む, ぶちこむ)", "definition3b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"], - ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 32, ["definition4a (打ち込む, ぶちこむ)", "definition4b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"] + ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 32, ["definition4a (打ち込む, ぶちこむ)", "definition4b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"], + ["画像", "がぞう", "tag1 tag2", "", 33, ["definition1a (画像, がぞう)", {"type": "image", "path": "image.gif", "width": 350, "height": 350, "description": "An image", "pixelated": true}], 7, "tag3 tag4 tag5"] ]
\ No newline at end of file diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json index 26922394..73d74e68 100644 --- a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json @@ -2,6 +2,12 @@ ["打", "freq", 1], ["打つ", "freq", 2], ["打ち込む", "freq", 3], + ["打", "freq", {"reading": "だ", "frequency": 4}], + ["打", "freq", {"reading": "ダース", "frequency": 5}], + ["打つ", "freq", {"reading": "うつ", "frequency": 6}], + ["打つ", "freq", {"reading": "ぶつ", "frequency": 7}], + ["打ち込む", "freq", {"reading": "うちこむ", "frequency": 8}], + ["打ち込む", "freq", {"reading": "ぶちこむ", "frequency": 9}], [ "打ち込む", "pitch", diff --git a/test/data/html/test-document1.html b/test/data/html/test-document1.html index 0754a314..98a6fb44 100644 --- a/test/data/html/test-document1.html +++ b/test/data/html/test-document1.html @@ -103,6 +103,7 @@ data-end-node-selector="img" data-end-offset="0" data-result-type="TextSourceElement" + data-sentence-extent="100" data-sentence="よみちゃん" > <img src="data:image/gif;base64,R0lGODdhBwAHAIABAAAAAP///ywAAAAABwAHAAACDIRvEaC32FpCbEkKCgA7" alt="よみちゃん" title="よみちゃん" style="width: 70px; height: 70px; image-rendering: crisp-edges; image-rendering: pixelated; display: block;" /> diff --git a/test/data/html/test-document2-frame1.html b/test/data/html/test-document2-frame1.html index 3b85cdc5..e572e3c4 100644 --- a/test/data/html/test-document2-frame1.html +++ b/test/data/html/test-document2-frame1.html @@ -33,10 +33,8 @@ a, a:visited { ありがとう </div> <div> - <a href="#" id="fullscreen-link">Toggle fullscreen</a> - <script> -document.querySelector('#fullscreen-link').addEventListener('click', () => toggleFullscreen(document.body), false); - </script> + <a href="#" class="fullscreen-link">Toggle fullscreen</a> + <script>setup(document.body, document.body);</script> </div> </div></body> </html>
\ No newline at end of file diff --git a/test/data/html/test-document2-script.js b/test/data/html/test-document2-script.js index bd5a570d..ab516a4e 100644 --- a/test/data/html/test-document2-script.js +++ b/test/data/html/test-document2-script.js @@ -39,3 +39,27 @@ function toggleFullscreen(element) { requestFullscreen(element); } } + +function setup(container, fullscreenElement=null) { + const fullscreenLink = container.querySelector('.fullscreen-link'); + if (fullscreenLink !== null) { + if (fullscreenElement === null) { + fullscreenElement = container.querySelector('.fullscreen-element'); + } + fullscreenLink.addEventListener('click', (e) => { + toggleFullscreen(fullscreenElement); + e.preventDefault(); + return false; + }, false); + } + + 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 content = document.importNode(template.content, true); + setup(content); + shadow.appendChild(content); + } +} diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html index 3a22a5bf..6d174571 100644 --- a/test/data/html/test-document2.html +++ b/test/data/html/test-document2.html @@ -11,71 +11,78 @@ <body> <h1>Yomichan Manual Tests</h1> - <p class="description">Manual tests involving fullscreen elements, <iframe>s, and shadow DOMs.</p> + <y-description>Manual tests involving fullscreen elements, <iframe>s, and shadow DOMs.</y-description> - <div class="test"> - <div class="description">Standard content.</div> - <div id="fullscreen-element1" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> + <y-test> + <y-description>Standard content.</y-description> + <div class="fullscreen-element" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> <div> ありがとう </div> <div> - <a href="#" id="fullscreen-link1">Toggle fullscreen</a> + <a href="#" class="fullscreen-link">Toggle fullscreen</a> </div> </div></div> - <script> -document.querySelector('#fullscreen-link1').addEventListener('click', () => toggleFullscreen(document.querySelector('#fullscreen-element1')), false); - </script> - </div> + </y-test> - <div class="test"> - <div class="description">Content inside of a shadow DOM.</div> - <div id="shadow-content-container"></div> - <template id="shadow-content-container-content-template"> + <y-test data-shadow-mode="open"> + <y-description>Content inside of an open shadow DOM.</y-description> + <div class="template-content-container"></div> + <template> <link rel="stylesheet" href="test-stylesheet.css" /> - <div id="fullscreen-element2" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> + <div class="fullscreen-element" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> <div> ありがとう </div> <div> - <a href="#" id="fullscreen-link2">Toggle fullscreen</a> + <a href="#" class="fullscreen-link">Toggle fullscreen</a> </div> </div></div> </template> - <script> -(() => { - const shadowIframeContainer = document.querySelector('#shadow-content-container'); - const shadow = shadowIframeContainer.attachShadow({mode: 'closed'}); - const template = document.querySelector('#shadow-content-container-content-template').content; - const content = document.importNode(template, true); - const fullscreenElement = content.querySelector('#fullscreen-element2'); - content.querySelector('#fullscreen-link2').addEventListener('click', () => toggleFullscreen(fullscreenElement), false); - shadow.appendChild(content); -})(); - </script> - </div> + </y-test> - <div class="test"> - <div class="description"><iframe> element.</div> + <y-test data-shadow-mode="closed"> + <y-description>Content inside of a closed shadow DOM.</y-description> + <div class="template-content-container"></div> + <template> + <link rel="stylesheet" href="test-stylesheet.css" /> + <div class="fullscreen-element" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> + <div> + ありがとう + </div> + <div> + <a href="#" class="fullscreen-link">Toggle fullscreen</a> + </div> + </div></div> + </template> + </y-test> + + <y-test> + <y-description><iframe> element.</y-description> <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> - </div> + </y-test> + + <y-test data-shadow-mode="open"> + <y-description><iframe> element inside of an open shadow DOM.</y-description> + <div class="template-content-container"></div> + <template> + <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> + </template> + </y-test> - <div class="test"> - <div class="description"><iframe> element inside of a shadow DOM.</div> - <div id="shadow-iframe-container"></div> - <template id="shadow-iframe-container-content-template"> + <y-test data-shadow-mode="closed"> + <y-description><iframe> element inside of a closed shadow DOM.</y-description> + <div class="template-content-container"></div> + <template> <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> </template> - <script> -(() => { - const shadowIframeContainer = document.querySelector('#shadow-iframe-container'); - const shadow = shadowIframeContainer.attachShadow({mode: 'closed'}); - const template = document.querySelector('#shadow-iframe-container-content-template').content; - const content = document.importNode(template, true); - shadow.appendChild(content); -})(); - </script> - </div> + </y-test> + + <script> +for (const element of document.querySelectorAll('y-test')) { + setup(element); +} + </script> </body> -</html>
\ No newline at end of file +</html> diff --git a/test/data/html/test-document3-frame1.html b/test/data/html/test-document3-frame1.html new file mode 100644 index 00000000..2ae906d2 --- /dev/null +++ b/test/data/html/test-document3-frame1.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Manual Performance Tests</title> + <link rel="stylesheet" href="test-stylesheet.css" /> + </head> +<body><div class="content"> + + <div class="description">Add elements</div> + + <div> + <a href="#" id="add-elements-1000">1000</a> + <a href="#" id="add-elements-10000">10000</a> + <a href="#" id="add-elements-100000">100000</a> + <a href="#" id="add-elements-1000000">1000000</a> + <script> +document.querySelector('#add-elements-1000').addEventListener('click', () => addElements(1000), false); +document.querySelector('#add-elements-10000').addEventListener('click', () => addElements(10000), false); +document.querySelector('#add-elements-100000').addEventListener('click', () => addElements(100000), false); +document.querySelector('#add-elements-1000000').addEventListener('click', () => addElements(1000000), false); + +let counter = 0; + +function addElements(amount) { + const container = document.querySelector('#container'); + for (let i = 0; i < amount; i++) { + const element = document.createElement('div'); + element.textContent = 'ありがとう'; + container.appendChild(element); + } + + counter += amount; + document.querySelector('#counter').textContent = counter; +} + </script> + </div> + + <div id="counter"></div> + <div id="container"></div> + +</div></body> +</html> diff --git a/test/data/html/test-document3-frame2.html b/test/data/html/test-document3-frame2.html new file mode 100644 index 00000000..c486e04b --- /dev/null +++ b/test/data/html/test-document3-frame2.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Manual Performance Tests</title> + <link rel="stylesheet" href="test-stylesheet.css" /> + </head> +<body><div class="content"> + + <div class="description"><iframe> element inside of an open shadow DOM.</div> + + <div id="shadow-iframe-container-open"></div> + <template id="shadow-iframe-container-open-content-template"> + <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 50px; border: 1px solid #d8d8d8;"></iframe> + </template> + <script> +(() => { + const shadowIframeContainer = document.querySelector('#shadow-iframe-container-open'); + const shadow = shadowIframeContainer.attachShadow({mode: 'open'}); + const template = document.querySelector('#shadow-iframe-container-open-content-template').content; + const content = document.importNode(template, true); + shadow.appendChild(content); +})(); + </script> + + <div class="description">Add elements</div> + + <div> + <a href="#" id="add-elements-1000">1000</a> + <a href="#" id="add-elements-10000">10000</a> + <a href="#" id="add-elements-100000">100000</a> + <a href="#" id="add-elements-1000000">1000000</a> + </div> + + <div id="counter"></div> + <div id="container"></div> + <script> +(() => { + document.querySelector('#add-elements-1000').addEventListener('click', () => addElements(1000), false); + document.querySelector('#add-elements-10000').addEventListener('click', () => addElements(10000), false); + document.querySelector('#add-elements-100000').addEventListener('click', () => addElements(100000), false); + document.querySelector('#add-elements-1000000').addEventListener('click', () => addElements(1000000), false); + + let counter = 0; + + function addElements(amount) { + const container = document.querySelector('#container'); + for (let i = 0; i < amount; i++) { + const element = document.createElement('div'); + element.textContent = 'ありがとう'; + container.appendChild(element); + } + + counter += amount; + document.querySelector('#counter').textContent = counter; + } +})(); + </script> + +</div></body> +</html> diff --git a/test/data/html/test-document3.html b/test/data/html/test-document3.html new file mode 100644 index 00000000..3e7d5236 --- /dev/null +++ b/test/data/html/test-document3.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Manual Performance Tests</title> + <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" /> + <link rel="stylesheet" href="test-stylesheet.css" /> + </head> +<body> + + <h1>Yomichan Manual Performance Tests</h1> + <p class="description">Testing Yomichan performance with artificially demanding cases in a real browser</p> + + <div class="test"> + <div class="description"><iframe> element.</div> + <iframe src="test-document3-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> + </div> + + <div class="test"> + <div class="description"><iframe> element containing an <iframe> element inside of an open shadow DOM.</div> + <iframe src="test-document3-frame2.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> + </div> + +</body> +</html> diff --git a/test/data/html/test-dom-text-scanner.html b/test/data/html/test-dom-text-scanner.html new file mode 100644 index 00000000..dc06eb64 --- /dev/null +++ b/test/data/html/test-dom-text-scanner.html @@ -0,0 +1,393 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan DOMTextScanner Tests</title> + <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" /> + <link rel="stylesheet" href="test-stylesheet.css" /> + </head> +<body> + + <h1>Yomichan DOMTextScanner Tests</h1> + + <y-test + data-test-data='{ + "node": "div:nth-of-type(1)", + "offset": 0, + "length": 15, + "expected": { + "node": "div:nth-of-type(2)>div::text", + "offset": 3, + "content": "小ぢん\nまり1\n小ぢん\nまり2" + } + }' + > + <y-description>Layout newlines expected due to entering and exiting display:block nodes.</y-description> +<div><div>小ぢん</div>まり1</div> +<div>小ぢん<div>まり2</div></div> + </y-test> + + <y-test + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 13, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n小ぢんまり2" + } + }' + > + <y-description>Layout newline expected due to sequential display:block elements.</y-description> +<div>小ぢんまり1</div><div>小ぢんまり2</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 13, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n小ぢんまり2" + } + }' + > + <y-description>Layout newline expected due to sequential display:block elements separated by a newline.</y-description> +<div>小ぢんまり1</div> +<div>小ぢんまり2</div> + </y-test> + + <y-test + data-test-data='{ + "node": "span:nth-of-type(1)::text", + "offset": 0, + "length": 12, + "expected": { + "node": "span:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1小ぢんまり2" + } + }' + > + <y-description>No newlines expected due to display:inline.</y-description> +<span>小ぢんまり1</span><span>小ぢんまり2</span> + </y-test> + + <y-test + data-test-data='{ + "node": "span:nth-of-type(1)::text", + "offset": 0, + "length": 13, + "expected": { + "node": "span:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1 小ぢんまり2" + } + }' + > + <y-description>No newlines expected due to white-space:normal.</y-description> +<span>小ぢんまり1</span> +<span>小ぢんまり2</span> + </y-test> + + <y-test + data-test-data='{ + "node": "span:nth-of-type(1)::text", + "offset": 0, + "length": 13, + "expected": { + "node": "span:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n小ぢんまり2" + } + }' + > + <y-description>Newline expected due to white-space:pre.</y-description> +<pre> +<span>小ぢんまり1</span> +<span>小ぢんまり2</span> +</pre> + </y-test> + + <y-test + data-test-data='{ + "node": "span:nth-of-type(1)::text", + "offset": 0, + "length": 12, + "expected": { + "node": "span:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1小ぢんまり2" + } + }' + > + <y-description>No newlines expected due to display:inline-block. Actual layout flow cannot be determined by DOM/CSS alone.</y-description> +<span style="display: inline-block;">小ぢんまり1</span><span style="display: inline-block;">小ぢんまり2</span> + </y-test> + + <y-test + style="position: relative;" + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 13, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n小ぢんまり2" + } + }' + > + <y-description>Single newline expected due to display:block layout.</y-description> +<div>小ぢんまり1</div><div style="position: relative;">小ぢんまり2</div> + </y-test> + + <y-test + style="position: relative; overflow: hidden;" + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 14, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n\n小ぢんまり2" + } + }' + > + <y-description>Two newlines expected due to position:absolute causing a significant layout change.</y-description> +<div>小ぢんまり1</div><div style="position: absolute;">小ぢんまり2</div> + </y-test> + + <y-test + style="position: relative; overflow: hidden;" + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 14, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n\n小ぢんまり2" + } + }' + > + <y-description>Two newlines expected due to position:fixed causing a significant layout change.</y-description> +<div>小ぢんまり1</div><div style="position: fixed;">小ぢんまり2</div> + </y-test> + + <y-test + style="position: relative;" + data-test-data='{ + "node": "div:nth-of-type(1)::text", + "offset": 0, + "length": 14, + "expected": { + "node": "div:nth-of-type(2)::text", + "offset": 6, + "content": "小ぢんまり1\n\n小ぢんまり2" + } + }' + > + <y-description>Two newlines expected due to position:sticky being able to cause a significant layout change.</y-description> +<div>小ぢんまり1</div><div style="position: sticky;">小ぢんまり2</div> + </y-test> + + <y-test + data-test-data='{ + "node": "rt", + "offset": 0, + "length": 6, + "expected": { + "node": "div::text", + "offset": 5, + "content": "小ぢんまり1" + } + }' + > + <y-description>Scanning text starting in an <rt> element. Should start scanning at the start of the <ruby> tag instead.</y-description> +<div><ruby>小<rp>(</rp><rt>こ</rt><rp>)</rp></ruby>ぢんまり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip <script> content.</y-description> +<div>小ぢん<script>/*comment*/</script>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip <style> content.</y-description> +<div>小ぢん<style>/*comment*/</style>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip <textarea> content.</y-description> +<div>小ぢん<textarea>textarea content</textarea>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip <input> content.</y-description> +<div>小ぢん<input value="content" />まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip <button> content.</y-description> +<div>小ぢん<button>content</button>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip content with font-size:0.</y-description> +<div>小ぢん<span style="font-size: 0;">content</span>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip content with opacity:0.</y-description> +<div>小ぢん<span style="opacity: 0;">content</span>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip content with visibility:hidden.</y-description> +<div>小ぢん<span style="visibility: hidden;">content</span>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip content with display:none.</y-description> +<div>小ぢん<span style="display: none;">content</span>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Don't skip content with user-select:none.</y-description> +<div>小ぢ<span style="user-select: none;">ん</span>まり1</div> + </y-test> + + <y-test + data-test-data='{ + "node": "div", + "offset": 0, + "length": 6, + "expected": { + "node": "div::nth-text(2)", + "offset": 3, + "content": "小ぢんまり1" + } + }' + > + <y-description>Skip content with user-select:none <em>and</em> a transparent color.</y-description> +<div>小ぢん<span style="user-select: none; color: rgba(0, 0, 0, 0);">content</span>まり1</div> + </y-test> + +</body> +</html>
\ No newline at end of file diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css index f63d2481..b4d2e255 100644 --- a/test/data/html/test-stylesheet.css +++ b/test/data/html/test-stylesheet.css @@ -19,7 +19,8 @@ p { margin: 0.33em 0; } -h1+p { +h1+p, +h1+y-description { margin-top: -0.67em; } @@ -28,7 +29,9 @@ a, a:visited { text-decoration: underline; } -.test { +.test, +y-test { + display: block; background-color: #ffffff; margin: 1em 0; padding: 0.5em; @@ -36,7 +39,8 @@ a, a:visited { border-radius: 4px; } -.test:before { +.test:before, +y-test:before { content: "Test " counter(test-id); display: block; counter-increment: test-id; @@ -45,7 +49,10 @@ a, a:visited { font-weight: bold; } -.description { +.description, +y-description { color: #444444; font-style: italic; + display: block; + padding-bottom: 0.5em; } diff --git a/test/test-database.js b/test/test-database.js index d27f92e1..e8a4a343 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -92,13 +92,63 @@ class XMLHttpRequest { } } +class Image { + constructor() { + this._src = ''; + this._loadCallbacks = []; + } + + get src() { + return this._src; + } + + set src(value) { + this._src = value; + this._delayTriggerLoad(); + } + + get naturalWidth() { + return 100; + } + + get naturalHeight() { + return 100; + } + + addEventListener(eventName, callback) { + if (eventName === 'load') { + this._loadCallbacks.push(callback); + } + } + + removeEventListener(eventName, callback) { + if (eventName === 'load') { + const index = this._loadCallbacks.indexOf(callback); + if (index >= 0) { + this._loadCallbacks.splice(index, 1); + } + } + } + + async _delayTriggerLoad() { + await Promise.resolve(); + for (const callback of this._loadCallbacks) { + callback(); + } + } +} + const vm = new VM({ chrome, + Image, XMLHttpRequest, indexedDB: global.indexedDB, IDBKeyRange: global.IDBKeyRange, - JSZip: yomichanTest.JSZip + JSZip: yomichanTest.JSZip, + addEventListener() { + // NOP + } }); vm.context.window = vm.context; @@ -106,6 +156,7 @@ vm.execute([ 'bg/js/json-schema.js', 'bg/js/dictionary.js', 'mixed/js/core.js', + 'bg/js/media-utility.js', 'bg/js/request.js', 'bg/js/dictionary-importer.js', 'bg/js/database.js' @@ -182,10 +233,10 @@ async function testDatabase1() { let progressEvent = false; await database.deleteDictionary( title, + {rate: 1000}, () => { progressEvent = true; - }, - {rate: 1000} + } ); assert.ok(progressEvent); @@ -216,10 +267,10 @@ async function testDatabase1() { const {result, errors} = await dictionaryImporter.import( database, testDictionarySource, + {prefixWildcardsSupported: true}, () => { progressEvent = true; - }, - {prefixWildcardsSupported: true} + } ); vm.assert.deepStrictEqual(errors, []); vm.assert.deepStrictEqual(result, expectedSummary); @@ -235,8 +286,8 @@ async function testDatabase1() { true ); vm.assert.deepStrictEqual(counts, { - counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}], - total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14} + counts: [{kanji: 2, kanjiMeta: 2, terms: 33, termMeta: 12, tagMeta: 14}], + total: {kanji: 2, kanjiMeta: 2, terms: 33, termMeta: 12, tagMeta: 14} }); // Test find* functions @@ -626,9 +677,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 1, + total: 3, modes: [ - ['freq', 1] + ['freq', 3] ] } }, @@ -639,9 +690,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 1, + total: 3, modes: [ - ['freq', 1] + ['freq', 3] ] } }, @@ -652,9 +703,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 3, + total: 5, modes: [ - ['freq', 1], + ['freq', 3], ['pitch', 2] ] } @@ -857,7 +908,7 @@ async function testDatabase2() { // Error: not prepared await assert.rejects(async () => await database.purge()); - await assert.rejects(async () => await database.deleteDictionary(title, () => {}, {})); + await assert.rejects(async () => await database.deleteDictionary(title, {}, () => {})); await assert.rejects(async () => await database.findTermsBulk(['?'], titles, null)); await assert.rejects(async () => await database.findTermsExactBulk(['?'], ['?'], titles)); await assert.rejects(async () => await database.findTermsBySequenceBulk([1], title)); @@ -868,17 +919,17 @@ async function testDatabase2() { await assert.rejects(async () => await database.findTagForTitle('tag', title)); await assert.rejects(async () => await database.getDictionaryInfo()); await assert.rejects(async () => await database.getDictionaryCounts(titles, true)); - await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {})); + await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, {}, () => {})); await database.prepare(); // Error: already prepared await assert.rejects(async () => await database.prepare()); - await dictionaryImporter.import(database, testDictionarySource, () => {}, {}); + await dictionaryImporter.import(database, testDictionarySource, {}, () => {}); // Error: dictionary already imported - await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {})); + await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, {}, () => {})); await database.close(); } @@ -905,7 +956,7 @@ async function testDatabase3() { let error = null; try { - await dictionaryImporter.import(database, testDictionarySource, () => {}, {}); + await dictionaryImporter.import(database, testDictionarySource, {}, () => {}); } catch (e) { error = e; } diff --git a/test/test-dom-text-scanner.js b/test/test-dom-text-scanner.js new file mode 100644 index 00000000..7374ff87 --- /dev/null +++ b/test/test-dom-text-scanner.js @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 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 {VM} = require('./yomichan-vm'); + + +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + return new JSDOM(domSource); +} + +function querySelectorTextNode(element, selector) { + let textIndex = -1; + const match = /::text$|::nth-text\((\d+)\)$/.exec(selector); + if (match !== null) { + textIndex = (match[1] ? parseInt(match[1], 10) - 1 : 0); + selector = selector.substring(0, selector.length - match[0].length); + } + const result = element.querySelector(selector); + if (textIndex < 0) { + return result; + } + for (let n = result.firstChild; n !== null; n = n.nextSibling) { + if (n.nodeType === n.constructor.TEXT_NODE) { + if (textIndex === 0) { + return n; + } + --textIndex; + } + } + return null; +} + + +function getComputedFontSizeInPixels(window, getComputedStyle, element) { + for (; element !== null; element = element.parentNode) { + if (element.nodeType === window.Node.ELEMENT_NODE) { + const fontSize = getComputedStyle(element).fontSize; + if (fontSize.endsWith('px')) { + const value = parseFloat(fontSize.substring(0, fontSize.length - 2)); + return value; + } + } + } + const defaultFontSize = 14; + return defaultFontSize; +} + +function createAbsoluteGetComputedStyle(window) { + // Wrapper to convert em units to px units + const getComputedStyleOld = window.getComputedStyle.bind(window); + return (element, ...args) => { + const style = getComputedStyleOld(element, ...args); + return new Proxy(style, { + get: (target, property) => { + let result = target[property]; + if (typeof result === 'string') { + result = result.replace(/([-+]?\d(?:\.\d)?(?:[eE][-+]?\d+)?)em/g, (g0, g1) => { + const fontSize = getComputedFontSizeInPixels(window, getComputedStyleOld, element); + return `${parseFloat(g1) * fontSize}px`; + }); + } + return result; + } + }); + }; +} + + +async function testDomTextScanner(dom, {DOMTextScanner}) { + const document = dom.window.document; + for (const testElement of document.querySelectorAll('y-test')) { + let testData = JSON.parse(testElement.dataset.testData); + if (!Array.isArray(testData)) { + testData = [testData]; + } + for (const testDataItem of testData) { + let { + node, + offset, + length, + forcePreserveWhitespace, + generateLayoutContent, + reversible, + expected: { + node: expectedNode, + offset: expectedOffset, + content: expectedContent, + remainder: expectedRemainder + } + } = testDataItem; + + node = querySelectorTextNode(testElement, node); + expectedNode = querySelectorTextNode(testElement, expectedNode); + + // Standard test + { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length); + + const {node: actualNode1, offset: actualOffset1, content: actualContent1, remainder: actualRemainder1} = scanner; + assert.strictEqual(actualContent1, expectedContent); + assert.strictEqual(actualOffset1, expectedOffset); + assert.strictEqual(actualNode1, expectedNode); + assert.strictEqual(actualRemainder1, expectedRemainder || 0); + } + + // Substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length - i); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(0, expectedContent.length - i)); + } + + if (reversible === false) { continue; } + + // Reversed test + { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-length); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent); + } + + // Reversed substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-(length - i)); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(i)); + } + } + } +} + + +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html')); + const window = dom.window; + try { + const {document, Node, Range} = window; + + window.getComputedStyle = createAbsoluteGetComputedStyle(window); + + const vm = new VM({document, window, Range, Node}); + vm.execute('fg/js/dom-text-scanner.js'); + const DOMTextScanner = vm.get('DOMTextScanner'); + + await testDomTextScanner(dom, {DOMTextScanner}); + } finally { + window.close(); + } +} + + +async function main() { + await testDocument1(); +} + + +if (require.main === module) { main(); } diff --git a/test/test-japanese.js b/test/test-japanese.js index 87efdfad..39004128 100644 --- a/test/test-japanese.js +++ b/test/test-japanese.js @@ -22,8 +22,7 @@ const vm = new VM(); vm.execute([ 'mixed/lib/wanakana.min.js', 'mixed/js/japanese.js', - 'bg/js/text-source-map.js', - 'bg/js/japanese.js' + 'bg/js/text-source-map.js' ]); const jp = vm.get('jp'); const TextSourceMap = vm.get('TextSourceMap'); @@ -434,17 +433,17 @@ function testIsMoraPitchHigh() { [[2, 1], false], [[3, 1], false], - [[0, 2], true], + [[0, 2], false], [[1, 2], true], [[2, 2], false], [[3, 2], false], - [[0, 3], true], + [[0, 3], false], [[1, 3], true], [[2, 3], true], [[3, 3], false], - [[0, 4], true], + [[0, 4], false], [[1, 4], true], [[2, 4], true], [[3, 4], true] diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js index 0773ba6e..1e694946 100644 --- a/test/test-object-property-accessor.js +++ b/test/test-object-property-accessor.js @@ -40,29 +40,30 @@ function createTestObject() { } -function testGetProperty1() { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - +function testGet1() { const data = [ - [[], object], - [['0'], object['0']], - [['value1'], object.value1], - [['value1', 'value2'], object.value1.value2], - [['value1', 'value3'], object.value1.value3], - [['value1', 'value4'], object.value1.value4], - [['value5'], object.value5], - [['value5', 0], object.value5[0]], - [['value5', 1], object.value5[1]], - [['value5', 2], object.value5[2]] + [[], (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, expected] of data) { - assert.strictEqual(accessor.getProperty(pathArray), expected); + for (const [pathArray, getExpected] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + const expected = getExpected(object); + + assert.strictEqual(accessor.get(pathArray), expected); } } -function testGetProperty2() { +function testGet2() { const object = createTestObject(); const accessor = new ObjectPropertyAccessor(object); @@ -89,15 +90,12 @@ function testGetProperty2() { ]; for (const [pathArray, message] of data) { - assert.throws(() => accessor.getProperty(pathArray), {message}); + assert.throws(() => accessor.get(pathArray), {message}); } } -function testSetProperty1() { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - +function testSet1() { const testValue = {}; const data = [ ['0'], @@ -112,17 +110,21 @@ function testSetProperty1() { ]; for (const pathArray of data) { - accessor.setProperty(pathArray, testValue); - assert.strictEqual(accessor.getProperty(pathArray), testValue); + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + accessor.set(pathArray, testValue); + assert.strictEqual(accessor.get(pathArray), testValue); } } -function testSetProperty2() { +function testSet2() { const object = createTestObject(); const accessor = new ObjectPropertyAccessor(object); const testValue = {}; const data = [ + [[], 'Invalid path'], [[0], 'Invalid path: [0]'], [['0', 'invalid'], 'Invalid path: ["0"].invalid'], [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], @@ -137,7 +139,127 @@ function testSetProperty2() { ]; for (const [pathArray, message] of data) { - assert.throws(() => accessor.setProperty(pathArray, testValue), {message}); + assert.throws(() => accessor.set(pathArray, testValue), {message}); + } +} + + +function testDelete1() { + const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property); + + 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 ObjectPropertyAccessor(object); + + accessor.delete(pathArray); + assert.ok(validate(object)); + } +} + +function testDelete2() { + 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 ObjectPropertyAccessor(object); + + assert.throws(() => accessor.delete(pathArray), {message}); + } +} + + +function testSwap1() { + 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 ObjectPropertyAccessor(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() { + 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 ObjectPropertyAccessor(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); } } @@ -272,10 +394,14 @@ function testIsValidPropertyType() { function main() { - testGetProperty1(); - testGetProperty2(); - testSetProperty1(); - testSetProperty2(); + testGet1(); + testGet2(); + testSet1(); + testSet2(); + testDelete1(); + testDelete2(); + testSwap1(); + testSwap2(); testGetPathString1(); testGetPathString2(); testGetPathArray1(); diff --git a/test/yomichan-test.js b/test/yomichan-test.js index 3351ecdf..b4f5ac7c 100644 --- a/test/yomichan-test.js +++ b/test/yomichan-test.js @@ -38,12 +38,17 @@ function createTestDictionaryArchive(dictionary, dictionaryName) { const archive = new (getJSZip())(); for (const fileName of fileNames) { - const source = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'}); - const json = JSON.parse(source); - if (fileName === 'index.json' && typeof dictionaryName === 'string') { - json.title = dictionaryName; + if (/\.json$/.test(fileName)) { + const content = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'}); + const json = JSON.parse(content); + if (fileName === 'index.json' && typeof dictionaryName === 'string') { + json.title = dictionaryName; + } + archive.file(fileName, JSON.stringify(json, null, 0)); + } else { + const content = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: null}); + archive.file(fileName, content); } - archive.file(fileName, JSON.stringify(json, null, 0)); } return archive; |