166 lines
7.6 KiB
Python
166 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
STATIC_ROOT = PROJECT_ROOT / "arr_web" / "static"
|
|
|
|
|
|
class DailyPriceReviewUiContractTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.html = (STATIC_ROOT / "index.html").read_text(encoding="utf-8")
|
|
cls.script = (STATIC_ROOT / "app.js").read_text(encoding="utf-8")
|
|
cls.styles = (STATIC_ROOT / "styles.css").read_text(encoding="utf-8")
|
|
cls.i18n = (STATIC_ROOT / "i18n.js").read_text(encoding="utf-8")
|
|
|
|
def test_needs_review_status_is_the_review_action(self) -> None:
|
|
render_start = self.script.index("function dailyJobStatus(job)")
|
|
render_end = self.script.index("function dailyJobDownload(job)", render_start)
|
|
renderer = self.script[render_start:render_end]
|
|
self.assertIn('needs_review: ["status.needs_review", "review"]', self.script)
|
|
self.assertIn('return job.status === "needs_review" && job.review_case_id', renderer)
|
|
self.assertIn('data-open-price-review=', renderer)
|
|
self.assertIn('${chip(job.status, jobStatus)}<span class="job-review-progress">', renderer)
|
|
self.assertNotIn("const reviewProgress", renderer)
|
|
|
|
handler_start = self.script.index('$("#jobs-body").addEventListener("click"')
|
|
handler_end = self.script.index('$("#daily-price-review-body")', handler_start)
|
|
handlers = self.script[handler_start:handler_end]
|
|
self.assertIn("openDailyPriceReview(reviewTrigger.dataset.openPriceReview, true)", handlers)
|
|
self.assertNotIn('$("#jobs-body").addEventListener("keydown"', handlers)
|
|
self.assertNotIn('event.target.closest("a, button")', handlers)
|
|
self.assertNotIn('closest("[data-job-id]")', handlers)
|
|
self.assertIn('id="daily-price-review-panel" aria-labelledby="daily-price-review-title" tabindex="-1"', self.html)
|
|
self.assertIn("panel.focus({ preventScroll: true });", self.script)
|
|
|
|
def test_explanatory_copy_and_metrics_are_removed_while_price_keys_and_controls_remain(self) -> None:
|
|
removed = (
|
|
"冻结清单已保留,但上次技术生成未完成。"
|
|
"可再次确认重试,不能再修改价格。"
|
|
)
|
|
self.assertNotIn(removed, self.html)
|
|
self.assertNotIn(removed, self.script)
|
|
self.assertNotIn(removed, self.i18n)
|
|
for element_id in ("daily-price-review-copy", "daily-price-review-completed", "daily-price-review-pending"):
|
|
self.assertNotIn(f'id="{element_id}"', self.html)
|
|
self.assertNotIn(f'$("#{element_id}")', self.script)
|
|
start = self.html.index('class="daily-price-review-table"')
|
|
end = self.html.index("</thead>", start)
|
|
heading = self.html[start:end]
|
|
self.assertEqual(heading.count('<th scope="col">'), 5)
|
|
for key in ("公司", "Rate Code", "Opera 价", "人工价格", "操作"):
|
|
self.assertIn(key, heading)
|
|
for key in ("固定价候选", "影响记录", "房数 / 房晚"):
|
|
self.assertNotIn(key, heading)
|
|
self.assertIn('data-daily-price-review-save', self.script)
|
|
self.assertIn('aria-describedby="daily-price-review-message"', self.script)
|
|
|
|
def test_finalize_sets_upload_card_to_generating_before_request(self) -> None:
|
|
start = self.script.index("async function finalizeDailyPriceReview()")
|
|
end = self.script.index("function dailyPriceReviewCancelIsOpen()", start)
|
|
finalize = self.script[start:end]
|
|
progress = 'setUploadProgress(100, I18N?.t("upload.daily_generating")'
|
|
request = 'api(`/api/jobs/${encodeURIComponent(jobId)}/review/finalize`'
|
|
self.assertIn(progress, finalize)
|
|
self.assertLess(finalize.index(progress), finalize.index(request))
|
|
self.assertIn('I18N?.t("upload.waiting_manual_price")', self.script)
|
|
|
|
def test_review_catalog_has_complete_three_locale_entries(self) -> None:
|
|
required = {
|
|
"daily_review.open",
|
|
"daily_review.progress_short",
|
|
"daily_review.step",
|
|
"daily_review.title",
|
|
"daily_review.default_instruction",
|
|
"daily_review.processing_instruction",
|
|
"daily_review.progress_aria",
|
|
"daily_review.completed",
|
|
"daily_review.pending",
|
|
"daily_review.cancel",
|
|
"daily_review.finalize",
|
|
"daily_review.finalizing",
|
|
"daily_review.retry",
|
|
"daily_review.items_aria",
|
|
"daily_review.opera_price",
|
|
"daily_review.fixed_candidates",
|
|
"daily_review.affected_records",
|
|
"daily_review.rooms_nights",
|
|
"daily_review.manual_price",
|
|
"daily_review.price_placeholder",
|
|
"daily_review.no_candidates",
|
|
"daily_review.manual_price_label",
|
|
"daily_review.saved",
|
|
"daily_review.awaiting_input",
|
|
"daily_review.save",
|
|
"daily_review.empty",
|
|
"daily_review.pagination_aria",
|
|
"daily_review.pagination_zero",
|
|
"daily_review.pagination_summary",
|
|
"daily_review.pagination_page",
|
|
"daily_review.integer_required",
|
|
"daily_review.saving",
|
|
"daily_review.saved_message",
|
|
"daily_review.complete_first",
|
|
"daily_review.generation_message",
|
|
"daily_review.success",
|
|
"daily_review.generation_registered",
|
|
"daily_review.validation_failed",
|
|
"daily_review.cancelling",
|
|
"daily_review.cancelling_message",
|
|
"daily_review.cancelled",
|
|
"daily_review.cancel_dialog_title",
|
|
"daily_review.cancel_dialog_description",
|
|
"daily_review.cancel_dialog_note",
|
|
"daily_review.cancel_dialog_back",
|
|
"daily_review.cancel_dialog_confirm",
|
|
"upload.waiting_manual_price",
|
|
"upload.daily_generating",
|
|
"upload.review_required",
|
|
"status.needs_review",
|
|
}
|
|
for key in required:
|
|
match = re.search(rf'^\s*"{re.escape(key)}": (\[.*\]),$', self.i18n, re.MULTILINE)
|
|
self.assertIsNotNone(match, key)
|
|
values = json.loads(match.group(1))
|
|
self.assertEqual(len(values), 3, key)
|
|
self.assertTrue(all(values), key)
|
|
self.assertRegex(values[2], r"[\u0e00-\u0e7f]", key)
|
|
|
|
def test_review_api_errors_are_localized(self) -> None:
|
|
for code in (
|
|
"REVIEW_REQUEST_INVALID",
|
|
"REVIEW_NOT_FOUND",
|
|
"REVIEW_ITEM_NOT_FOUND",
|
|
"REVIEW_REVISION_CONFLICT",
|
|
"REVIEW_IMMUTABLE",
|
|
"REVIEW_NOT_OPEN",
|
|
"REVIEW_NOT_CANCELLABLE",
|
|
"REVIEW_RULESET_CHANGED",
|
|
"REVIEW_STATE_INVALID",
|
|
"REVIEW_INCOMPLETE",
|
|
"REVIEW_PRICE_INVALID",
|
|
"REVIEW_PAGE_INVALID",
|
|
"REVIEW_ACTOR_INVALID",
|
|
"REVIEW_GENERATION_FAILED",
|
|
):
|
|
self.assertIsNotNone(
|
|
re.search(rf'^\s*{code}: "error\.daily_review_[^"]+",$', self.i18n, re.MULTILINE),
|
|
code,
|
|
)
|
|
|
|
def test_review_prices_follow_active_locale_integer_format(self) -> None:
|
|
start = self.script.index("function reviewPrice(value)")
|
|
end = self.script.index("function reviewPriceInput(value)", start)
|
|
formatter = self.script[start:end]
|
|
self.assertIn("return formatInteger(numeric);", formatter)
|
|
self.assertNotIn('new Intl.NumberFormat("zh-CN"', formatter)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|