2
0

TipitakaReadingRenderTest.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. use App\Models\PaliText;
  3. use App\Models\Sentence;
  4. use App\Services\PaliContentService;
  5. use Illuminate\Foundation\Testing\RefreshDatabase;
  6. use Illuminate\Support\Facades\Cache;
  7. use Illuminate\Support\Str;
  8. uses(RefreshDatabase::class);
  9. /**
  10. * 建测试用的 channel 和句子:book 9001 的第 1 段两句,第 2 段一句。返回 channel uid
  11. */
  12. function makeParagraphFixture(): string
  13. {
  14. $channel = makeChannel(makeStudio('para-owner'), 'para channel');
  15. makeSentence($channel, 9001, 1, 1, 'first sentence');
  16. makeSentence($channel, 9001, 1, 2, 'second sentence');
  17. makeSentence($channel, 9001, 2, 1, 'other paragraph');
  18. return $channel;
  19. }
  20. it('renders every sentence of a paragraph wrapped in divs', function () {
  21. $channel = makeParagraphFixture();
  22. $data = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1")
  23. ->assertOk()
  24. ->json('data.0');
  25. expect($data['para'])->toBe(1);
  26. // 默认只输出 display
  27. expect($data)->not->toHaveKey('sentences');
  28. expect($data['display'])
  29. ->toContain("<div id='para-1' class='translation' data-para='1'>")
  30. ->toContain("<div class='sentence' data-sid='9001-1-1-1'>")
  31. ->toContain("<div class='sentence' data-sid='9001-1-2-2'>")
  32. ->toContain("<div class='para-block'>");
  33. });
  34. it('wraps a chapter title paragraph in a heading', function () {
  35. $channel = makeParagraphFixture();
  36. (new PaliText)->forceFill([
  37. 'book' => 9001,
  38. 'paragraph' => 1,
  39. 'level' => 2,
  40. 'class' => '',
  41. 'toc' => '',
  42. 'text' => '',
  43. 'html' => '',
  44. 'pcd_book_id' => 0,
  45. 'uid' => (string) Str::uuid(),
  46. ])->save();
  47. $display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1")
  48. ->assertOk()
  49. ->json('data.0.display');
  50. expect($display)->toContain('<h2>')->not->toContain('para-block');
  51. });
  52. it('can output only the sentences or both', function () {
  53. $channel = makeParagraphFixture();
  54. $sentences = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1&include=sentences")
  55. ->assertOk()
  56. ->json('data.0');
  57. expect($sentences)->not->toHaveKey('display');
  58. expect($sentences['sentences'])->toHaveCount(2);
  59. expect($sentences['sentences'][0])->toHaveKeys(['sid', 'html']);
  60. $all = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1&include=all")
  61. ->assertOk()
  62. ->json('data.0');
  63. expect($all)->toHaveKeys(['para', 'display', 'sentences']);
  64. });
  65. it('lists the paragraphs of a range and skips empty ones', function () {
  66. $channel = makeParagraphFixture();
  67. $items = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1&to=3")
  68. ->assertOk()
  69. ->json('data');
  70. expect(array_column($items, 'para'))->toBe([1, 2]);
  71. });
  72. it('outputs one line per sentence for non html formats', function () {
  73. $channel = makeParagraphFixture();
  74. $display = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=1&format=text")
  75. ->assertOk()
  76. ->json('data.0.display');
  77. expect($display)->toBe("first sentence\nsecond sentence");
  78. });
  79. it('rejects a malformed channel in the path but validates filters as 422', function () {
  80. $channel = makeParagraphFixture();
  81. // 路径参数由路由约束把关:不是 uuid 就匹配不上任何路由 → 404
  82. $this->getJson('/api/v3/tipitaka-reading/not-a-uuid')->assertStatus(404);
  83. // filter 是查询参数,走 FormRequest → 422
  84. $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=not-a-number")
  85. ->assertStatus(422)
  86. ->assertJsonStructure(['errors' => ['para']]);
  87. $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=not-a-number")
  88. ->assertStatus(422)
  89. ->assertJsonStructure(['errors' => ['book']]);
  90. // book 只在跟 chapter / para / to 一起用时必填
  91. $this->getJson("/api/v3/tipitaka-reading/{$channel}?para=1")
  92. ->assertStatus(422)
  93. ->assertJsonStructure(['errors' => ['book']]);
  94. // chapter 与 para 互斥
  95. $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&chapter=1&para=1")
  96. ->assertStatus(422);
  97. });
  98. it('fetches the whole channel when no filter is given', function () {
  99. $channel = makeParagraphFixture();
  100. // 不带任何查询串 = 取这个 channel 的全部译文,下载场景要的就是这个
  101. $json = $this->getJson("/api/v3/tipitaka-reading/{$channel}")->assertOk()->json();
  102. expect(array_column($json['data'], 'para'))->toBe([1, 2])
  103. ->and($json['data'][0]['book'])->toBe(9001)
  104. // 无 book 过滤时不给 total / remaining,靠 next_cursor 判断结束
  105. ->and($json['meta'])->not->toHaveKey('total')
  106. ->and($json['meta']['next_cursor'])->toBeNull();
  107. });
  108. it('returns an empty collection for a paragraph with no content', function () {
  109. $channel = makeParagraphFixture();
  110. // 这里是集合端点,没内容就是空集合而不是 404——区间口径下空段落本来就被跳过,
  111. // 单段只是区间长度为 1 的特例,两者口径要一致
  112. $json = $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=9")
  113. ->assertOk()
  114. ->json();
  115. expect($json['data'])->toBe([])
  116. ->and($json['meta'])->toMatchArray(['total' => 0, 'remaining' => 0])
  117. ->and($json['meta']['next_cursor'])->toBeNull();
  118. });
  119. it('rejects a range that ends before it starts', function () {
  120. $channel = makeParagraphFixture();
  121. $this->getJson("/api/v3/tipitaka-reading/{$channel}?book=9001&para=3&to=1")
  122. ->assertStatus(422)
  123. ->assertJsonPath('errors.to.0', __('site.invalid_parameter'));
  124. });
  125. it('caches the paragraph and drops the cache when a sentence changes', function () {
  126. $channel = makeParagraphFixture();
  127. $url = "/api/v3/tipitaka-reading/{$channel}?book=9001&para=1";
  128. $this->getJson($url)->assertOk();
  129. $key = PaliContentService::paragraphCacheKey(9001, 1, $channel, 'html');
  130. expect(Cache::has($key))->toBeTrue();
  131. $sentence = Sentence::where('book_id', 9001)->where('paragraph', 1)->orderBy('word_start')->first();
  132. $sentence->content = 'changed sentence';
  133. $sentence->save();
  134. expect(Cache::has($key))->toBeFalse();
  135. expect($this->getJson($url)->json('data.0.display'))->toContain('changed sentence');
  136. });
  137. it('drops the cache when a sentence is added or deleted', function () {
  138. $channel = makeParagraphFixture();
  139. $url = "/api/v3/tipitaka-reading/{$channel}?book=9001&para=1";
  140. $this->getJson($url)->assertOk();
  141. makeSentence($channel, 9001, 1, 3, 'third sentence');
  142. expect($this->getJson($url.'&include=sentences')->json('data.0.sentences'))->toHaveCount(3);
  143. Sentence::where('book_id', 9001)->where('paragraph', 1)->where('word_start', 3)->first()->delete();
  144. expect($this->getJson($url.'&include=sentences')->json('data.0.sentences'))->toHaveCount(2);
  145. });