ReadingAnnotationsTest.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. <?php
  2. use App\Facades\RedisCache;
  3. use App\Models\Discussion;
  4. use App\Services\PaliContentService;
  5. use Illuminate\Foundation\Testing\RefreshDatabase;
  6. use Illuminate\Support\Str;
  7. uses(RefreshDatabase::class);
  8. /**
  9. * 阅读页批注内嵌:type='commentary'(注释书对应,带出处)与 type='note'(普通边注,无出处)。
  10. */
  11. /** 建 channel + 一句根本译文(book 9101 第 1 段),返回 [channel uid, 句子] */
  12. function makeAnnotatedSentence(): array
  13. {
  14. $channel = makeChannel(makeStudio('anno-owner'), 'anno channel');
  15. $sentence = makeSentence($channel, 9101, 1, 1, 'first sentence');
  16. return [$channel, $sentence];
  17. }
  18. function makeAnnotation(string $resId, string $type, string $content, int $posEnd): Discussion
  19. {
  20. $discussion = new Discussion;
  21. $discussion->forceFill([
  22. 'res_id' => $resId,
  23. 'res_type' => 'sentence',
  24. 'type' => $type,
  25. 'title' => $content,
  26. 'content' => $content,
  27. 'content_type' => 'markdown',
  28. 'editor_uid' => makeStudio('anno-'.Str::random(6)),
  29. 'pos_start' => 0,
  30. 'pos_end' => $posEnd,
  31. 'quote_exact' => 'first',
  32. ])->save();
  33. return $discussion;
  34. }
  35. it('injects a commentary annotation with a cite link', function () {
  36. [$channel, $sentence] = makeAnnotatedSentence();
  37. // 义注句:commentary 的 content 是它的句子模板,渲染成它在同 channel 的译文
  38. makeSentence($channel, 9102, 7, 3, '义注这么说');
  39. makeAnnotation($sentence->uid, 'commentary', '{{9102-7-3-3}}', 5);
  40. $display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101&para=1")
  41. ->assertOk()
  42. ->json('data.0.display');
  43. expect($display)
  44. ->toContain('义注这么说')
  45. ->toContain('<cite class="anno-jump" data-book="9102" data-para="7" data-start="3" data-end="3">义注</cite>');
  46. });
  47. it('injects a plain note annotation without a cite link', function () {
  48. [$channel, $sentence] = makeAnnotatedSentence();
  49. // 普通边注:content 就是注解正文本身
  50. makeAnnotation($sentence->uid, 'note', '这里的 dassana 指见到佛陀。', 5);
  51. $display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101&para=1")
  52. ->assertOk()
  53. ->json('data.0.display');
  54. // 正文照原样进 sidenote,紧跟 </span> 收尾——没有 <cite> 出处
  55. expect($display)
  56. ->toContain('这里的 dassana 指见到佛陀。</span>')
  57. ->not->toContain('anno-jump');
  58. });
  59. it('injects both kinds on the same sentence, note first at the same position', function () {
  60. [$channel, $sentence] = makeAnnotatedSentence();
  61. makeSentence($channel, 9102, 7, 3, '义注这么说');
  62. makeAnnotation($sentence->uid, 'commentary', '{{9102-7-3-3}}', 5);
  63. makeAnnotation($sentence->uid, 'note', '译者按。', 5);
  64. $display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101&para=1")
  65. ->assertOk()
  66. ->json('data.0.display');
  67. expect(mb_strpos($display, '译者按。'))->toBeLessThan(mb_strpos($display, '义注这么说'));
  68. });
  69. it('drops the paragraph cache when either kind of annotation changes', function () {
  70. [$channel, $sentence] = makeAnnotatedSentence();
  71. $url = "/api/v3/tipitaka-reading/{$channel}?book=9101&para=1";
  72. $this->getJson($url)->assertOk();
  73. $key = PaliContentService::paragraphCacheKey(9101, 1, $channel, 'html');
  74. expect(RedisCache::has($key))->toBeTrue();
  75. makeAnnotation($sentence->uid, 'note', '译者按。', 5);
  76. expect(RedisCache::has($key))->toBeFalse();
  77. expect($this->getJson($url)->json('data.0.display'))->toContain('译者按。');
  78. });
  79. /* ------------------------------------------------------------------ glossary 与 issue */
  80. function makeAnchored(string $resId, string $type, array $attrs): Discussion
  81. {
  82. $discussion = new Discussion;
  83. $discussion->forceFill(array_merge([
  84. 'res_id' => $resId,
  85. 'res_type' => 'sentence',
  86. 'type' => $type,
  87. 'title' => 't',
  88. 'content' => 'body',
  89. 'content_type' => 'markdown',
  90. 'editor_uid' => makeStudio('anno-'.Str::random(6)),
  91. ], $attrs))->save();
  92. return $discussion;
  93. }
  94. function readFirstParagraph(string $channel): string
  95. {
  96. return test()->getJson("/api/v3/tipitaka-reading/{$channel}?book=9101&para=1")->assertOk()->json('data.0.display');
  97. }
  98. it('replaces a glossary anchor with a term reference', function () {
  99. [$channel, $sentence] = makeAnnotatedSentence(); // 'first sentence'
  100. makeAnchored($sentence->uid, 'glossary', ['title' => 'paṭhama', 'pos_start' => 0, 'pos_end' => 5, 'quote_exact' => 'first']);
  101. expect(readFirstParagraph($channel))
  102. ->toContain("data-term='paṭhama'")
  103. ->toContain('>first</span> sentence');
  104. });
  105. it('highlights issues, merging overlaps, and marks issues without a position at the sentence end', function () {
  106. [$channel, $sentence] = makeAnnotatedSentence();
  107. $a = makeAnchored($sentence->uid, 'issue', ['severity' => 'suggestion', 'pos_start' => 0, 'pos_end' => 8, 'quote_exact' => 'first se']);
  108. $b = makeAnchored($sentence->uid, 'issue', ['severity' => 'error', 'pos_start' => 6, 'pos_end' => 14, 'quote_exact' => 'sentence']);
  109. $c = makeAnchored($sentence->uid, 'issue', ['severity' => 'warning']);
  110. expect(readFirstParagraph($channel))
  111. ->toContain('<span class="anno-issue anno-issue-error" data-discussion-ids="'.$a->id.','.$b->id.'" data-severity="error">first sentence</span>')
  112. ->toContain('<span class="anno-issue-mark anno-issue-warning" data-discussion-ids="'.$c->id.'"');
  113. });
  114. it('relocates an issue by its quote when the position is stale', function () {
  115. [$channel, $sentence] = makeAnnotatedSentence();
  116. makeAnchored($sentence->uid, 'issue', ['severity' => 'error', 'pos_start' => 0, 'pos_end' => 3, 'quote_exact' => 'sentence']);
  117. expect(readFirstParagraph($channel))->toContain('data-severity="error">sentence</span>');
  118. });
  119. it('does not inject closed issues or glossaries, and drops the cache when one is closed', function () {
  120. [$channel, $sentence] = makeAnnotatedSentence();
  121. makeAnchored($sentence->uid, 'glossary', ['title' => 'x', 'pos_start' => 0, 'pos_end' => 5, 'quote_exact' => 'first', 'status' => 'close']);
  122. $issue = makeAnchored($sentence->uid, 'issue', ['severity' => 'error', 'pos_start' => 6, 'pos_end' => 14]);
  123. expect(readFirstParagraph($channel))->toContain('anno-issue')->not->toContain('term-ref');
  124. $issue->forceFill(['status' => 'close'])->save(); // 关闭会清缓存
  125. expect(readFirstParagraph($channel))->not->toContain('anno-issue');
  126. });
  127. it('leaves issue markup out of plain text output but keeps the glossary term', function () {
  128. [$channel, $sentence] = makeAnnotatedSentence();
  129. makeAnchored($sentence->uid, 'issue', ['severity' => 'error']);
  130. makeAnchored($sentence->uid, 'glossary', ['title' => 'paṭhama', 'pos_start' => 0, 'pos_end' => 5]);
  131. $text = app(PaliContentService::class)->readParagraph(9101, 1, $channel, 'text')['display'];
  132. expect($text)->toBe('first sentence');
  133. });