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 renderJobs(jobs)") render_end = self.script.index("function reviewPrice(value)", render_start) renderer = self.script[render_start:render_end] self.assertIn('needs_review: ["status.needs_review", "review"]', self.script) self.assertIn('const statusMarkup = job.status === "needs_review"', renderer) self.assertIn('data-open-price-review=', renderer) self.assertIn('${chip(job.status, jobStatus)}', 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.assertGreaterEqual(handlers.count('event.target.closest("a, button")'), 2) 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_retry_copy_is_removed_and_hidden(self) -> None: removed = ( "冻结清单已保留,但上次技术生成未完成。" "可再次确认重试,不能再修改价格。" ) self.assertNotIn(removed, self.html) self.assertNotIn(removed, self.script) self.assertNotIn(removed, self.i18n) self.assertIn("reviewCopy.hidden = retryable;", 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.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)