Simplify daily review UI and compact header cards

This commit is contained in:
Wyndham ARR committed 2026-10-09 14:30:41 +08:00
1 parent 6d1a98743f
commit 64f4a0d3de
13 files changed
+185 -133

No files matched your search

+26 -21
View File
@@ -318,21 +318,23 @@ test('pending date labels have Chinese, English and Thai translations',()=>{
assert.equal(catalog['arr_download.pending_price'][0],'待复核价格');
});
test('cancelled source rows need no fields and the exclusion stays visible before continuation',async()=>{
test('cancelled source exclusions need no fields or summary and still permit continuation',async()=>{
const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6}));
await h.acceptARRDownloadTask(task,{sync:false});
assert.equal(h.element('#arr-data-review-body').innerHTML,'');
assert.match(h.element('#arr-data-review-progress').textContent,/data_review\.excluded_cancelled/);
assert.equal(h.element('#arr-data-review-progress').textContent,'');
assert.equal(h.state.arrDataReview.excluded_cancelled_count,6);
assert.equal(h.element('#arr-data-review-finalize').disabled,false);
assert.equal(h.calls.every(call=>call.method==='GET'),true);
});
test('PM exclusions stay visible without requiring field completion',async()=>{
test('PM exclusions retain their original state without displaying a summary or requiring fields',async()=>{
const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6,excluded_pm_count:19}));
await h.acceptARRDownloadTask(task,{sync:false});
const progress=h.element('#arr-data-review-progress').textContent;
assert.match(progress,/data_review\.excluded_cancelled/);
assert.match(progress,/data_review\.excluded_pm/);
assert.equal(progress,'');
assert.equal(h.state.arrDataReview.excluded_cancelled_count,6);
assert.equal(h.state.arrDataReview.excluded_pm_count,19);
assert.equal(h.element('#arr-data-review-body').innerHTML,'');
assert.equal(h.element('#arr-data-review-finalize').disabled,false);
assert.equal(h.calls.every(call=>call.method==='GET'),true);
@@ -383,39 +385,41 @@ for (const fixture of [
{name:'confirmed decisions show the saved confirmation instead of the original problem',
source_state:'failed', reason_code:'http_failure', confirmed:true, value:'VERIFIED', expected:'confirmed_value'},
]) {
test(`field review explanation: ${fixture.name}`,async()=>{
test(`compact field review preserves decisions without explanatory metadata: ${fixture.name}`,async()=>{
const source=item(fixture);
const original=plain(source);
const h=harness(()=>response(200,review([source])));
await h.acceptARRDownloadTask(task,{sync:false});
const markup=h.element('#arr-data-review-body').innerHTML;
assert.match(markup,new RegExp(`data_review\\.${fixture.expected}\\b`));
assert.match(markup,new RegExp(`data_review\\.${source.can_be_empty?'optional':'required'}_field_rule\\b`));
assert.doesNotMatch(markup,/<small>|data_review\.(?:optional_field_rule|required_field_rule|source_row|room_missing_in_scope|source_unconfirmed|fetch_failed|conflict_value|invalid_value|confirmed_value)/);
assert.match(markup,new RegExp(`<label[^>]+>${source.field_label}</label>`));
assert.match(markup,/<strong>SYNTHETIC-001<\/strong>/);
assert.doesNotMatch(markup,/Synthetic Company|SYNTHETIC-ROOM|<small>/);
assert.equal((markup.match(/<td /g)||[]).length,4);
assert.match(markup,/data-arr-data-review-value/);
assert.match(markup,/data-arr-data-review-save/);
assert.match(markup,new RegExp(source.confirmed?'data_review.confirmed':'data_review.pending'));
assert.equal(markup.includes('data_review.missing_value'),false);
assert.equal(markup.includes('data-arr-data-review-empty'),source.can_be_empty);
assert.deepEqual(plain(source),original,'rendering explanations never changes a source or confirmation');
assert.deepEqual(plain(source),original,'a compact display never changes source metadata or confirmation');
assert.equal(h.calls.every(call=>call.method==='GET'),true);
});
}
test('review explanations have distinct Chinese, English and Thai copy for required, optional and failed reads',()=>{
test('compact review controls and validation messages remain available in Chinese, English and Thai',()=>{
const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}});
vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context);
const catalog=context.window.ARRI18n.CATALOG;
const keys=['required_field_rule','optional_field_rule','required_value_missing','room_missing_in_scope','source_unconfirmed',
'fetch_failed','conflict_value','invalid_value'];
const keys=['save','confirm_empty','confirmed','pending','field_block_code','field_disp_room_no','use_empty',
'complete_first','conflict','disconnected','invalid','required','integer_required'];
for(const key of keys) {
const translations=catalog[`data_review.${key}`];
assert.equal(translations.length,3);
translations.forEach(text=>assert.equal(typeof text==='string' && text.length>0,true));
}
assert.match(catalog['data_review.required_field_rule'][0],/原处理规则必填/);
assert.match(catalog['data_review.optional_field_rule'][0],/允许为空/);
assert.match(catalog['data_review.room_missing_in_scope'][0],/费率在处理范围内/);
assert.match(catalog['data_review.source_unconfirmed'][0],/确认无此信息/);
assert.match(catalog['data_review.fetch_failed'][0],/不能直接认定没有此信息/);
assert.equal(catalog['data_review.fetch_failed'][0].includes('刷新'),false,
'refreshing the review must not imply a retry of Oracle acquisition');
assert.equal(catalog['arr_acquisition.label'][0],'状态');
assert.equal(catalog['arr_acquisition.label'][1],'Status');
assert.equal(catalog['arr_download.date_independent'],undefined);
});
test('source review opens before a job exists and another date can start its own download',async()=>{
@@ -423,7 +427,8 @@ test('source review opens before a job exists and another date can start its own
? response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false})
: response(200,review([item()])));
await h.acceptARRDownloadTask(task,{sync:false});
assert.equal(h.element('#arr-download-data-review').hidden,false);
assert.equal(h.element('#arr-download-button').textContent,'arr_download.complete_data');
assert.equal(h.element('#arr-download-help').hidden,true);
assert.equal(h.element('#arr-data-review-panel').hidden,false);
assert.equal(h.element('#arr-download-review').hidden,true);
assert.equal(h.element('#arr-data-review-finalize').disabled,true);
@@ -586,7 +591,7 @@ test('revision conflict reloads the current revision before any next save',async
test('unconfirmed source values are never prefilled and all labels and stored values are escaped',async()=>{
const h=harness(()=>response(200,review([
item({company_name:'<img src=x onerror=alert(1)>',value:'GUESSED-BLOCK'}),
item({company_name:'<img src=x onerror=alert(1)>',confirmation_no:'<img src=x onerror=alert(1)>',value:'GUESSED-BLOCK'}),
item({item_id:'2:RES_COMMENT',field:'RES_COMMENT',confirmed:true,value:'</textarea><script>bad()</script>'}),
])));
await h.acceptARRDownloadTask(task,{sync:false});
+1 -1
View File
@@ -74,7 +74,7 @@ test('completed retrieval remains distinct from pending field and price review',
assert.equal(h.element('#arr-acquisition-status').textContent,'已完成');
assert.equal(h.element('#arr-acquisition-percent').textContent,'100%');
assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 85 / 85 笔');
if(status==='needs_data_review') assert.equal(h.element('#arr-download-data-review').hidden,false);
if(status==='needs_data_review') assert.equal(h.element('#arr-download-button').textContent,'完善报表字段');
if(status==='needs_review') assert.equal(h.element('#arr-download-review').hidden,false);
assert.doesNotMatch(h.element('#arr-acquisition-detail').textContent,/生成|报表已完成/);
}
+9 -1
View File
@@ -19,8 +19,16 @@ test('zero is a valid explicit price while blank and negative prices remain inva
test('zero is rendered as a saved value and permits continuation when all items are saved',()=>{
const h=harness(()=>{throw new Error('unexpected request');});
h.renderDailyPriceReview(fixture(0));
const review=fixture(0);
review.items[0].candidate_prices=[{effective_rate_amount:888888,real_price:999999}];
review.items[0].affected_records=333333;
h.renderDailyPriceReview(review);
const html=h.element('#daily-price-review-body').innerHTML;
assert.equal((html.match(/<td /g)||[]).length,5);
assert.match(html,/Fixture Company|FIXTURE/);
assert.match(html,/>100<\/td>/);
assert.doesNotMatch(html,/888888|999999|333333|daily-price-review-candidates/);
assert.match(html,/aria-describedby="daily-price-review-message"/);
assert.match(html,/value="0"/);
assert.match(html,/daily-price-review-row is-complete/);
assert.match(html,/daily_review.saved/);
@@ -62,6 +62,19 @@ test('a successful replacement displays only the accepted new report for the sam
assert.doesNotMatch(h.element('#jobs-body').innerHTML,/job_id=old/);
});
test('history access appears only when the date has historical versions while current download and delete remain available',()=>{
const h=setup();
for(const count of [0,undefined,1,2]) {
h.state.dailyReports=[day(job('current'),undefined,{history_count:count})];
h.renderJobs();
const html=h.element('#jobs-body').innerHTML;
assert.equal(html.includes(`data-daily-history="${date}"`),Number(count)>0);
assert.match(html,/job_id=current/);
assert.match(html,/data-daily-delete="current"/);
}
assert.equal(h.calls.length,0);
});
test('a failed replacement never removes the valid old report',()=>{
const h=setup();
h.state.dailyReports=[day(job('old'),job('new',{status:'failed',is_current:false,daily_report_sha256:null}))];
+14 -2
View File
@@ -38,7 +38,7 @@ class DailyPriceReviewUiContractTests(unittest.TestCase):
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:
def test_explanatory_copy_and_metrics_are_removed_while_price_keys_and_controls_remain(self) -> None:
removed = (
"冻结清单已保留,但上次技术生成未完成。"
"可再次确认重试,不能再修改价格。"
@@ -46,7 +46,19 @@ class DailyPriceReviewUiContractTests(unittest.TestCase):
self.assertNotIn(removed, self.html)
self.assertNotIn(removed, self.script)
self.assertNotIn(removed, self.i18n)
self.assertIn("reviewCopy.hidden = retryable;", self.script)
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()")
+7 -2
View File
@@ -47,7 +47,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
overview_markup,
)
self.assertIn(
".daily-overview { display: grid; grid-template-columns: minmax(220px, 1.15fr) minmax(230px, 1.2fr) minmax(155px, .8fr) minmax(125px, .65fr) minmax(220px, 1.15fr);",
".daily-overview { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.25fr) minmax(0, .85fr) minmax(0, .7fr) minmax(0, 1.2fr);",
self.styles,
)
self.assertIn(".daily-overview > .metric-card", self.styles)
@@ -61,7 +61,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
self.assertIn(".daily-overview { grid-template-columns: 1fr;", self.styles)
self.assertIn("@media (min-width: 701px)", self.styles)
self.assertIn(".daily-overview .dropzone {", self.styles)
self.assertIn("grid-template-columns: 28px minmax(0, 1fr)", self.styles)
self.assertIn("grid-template-columns: 20px minmax(0, 1fr)", self.styles)
self.assertIn("min-height: 72px", self.styles)
self.assertIn(".daily-overview .upload-footer", self.styles)
self.assertIn("min-height: 44px", self.styles)
@@ -69,6 +69,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
def test_upload_panel_has_compact_action_footer(self) -> None:
self.assertIn('class="panel-heading upload-heading"', self.html)
self.assertIn('class="upload-content"', self.html)
self.assertIn('class="upload-footer"', self.html)
self.assertIn(".upload-panel .primary-button", self.styles)
self.assertIn("min-width: 118px", self.styles)
@@ -152,6 +153,10 @@ class DailyVisualStaticContractTests(unittest.TestCase):
self.assertIn('<h2 id="arr-download-pending-title">需处理日期</h2>', self.html)
self.assertNotIn('<h2 id="daily-price-review-title">', self.html)
self.assertNotIn('<h2 id="arr-data-review-title">', self.html)
self.assertNotIn('id="arr-data-review-copy"', self.html)
self.assertNotIn('id="arr-data-review-progress"', self.html)
self.assertNotIn('id="arr-download-data-review"', self.html)
self.assertIn('id="arr-download-button" type="submit"', self.html)
def test_removed_upload_labels_do_not_return(self) -> None:
upload_start = self.html.index('class="panel upload-panel"')