Merge pull request 'тикер: разброс показанных строк под лентой — она больше не опровергает заголовок' (#3278) from fix/mera-ticker-context into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m36s
Deploy Trade-In / test (push) Successful in 4m45s
Deploy Trade-In / build-backend (push) Successful in 34s
Deploy Trade-In / deploy (push) Successful in 2m5s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m36s
Deploy Trade-In / test (push) Successful in 4m45s
Deploy Trade-In / build-backend (push) Successful in 34s
Deploy Trade-In / deploy (push) Successful in 2m5s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
This commit is contained in:
commit
98325d33a9
3 changed files with 85 additions and 23 deletions
|
|
@ -10,6 +10,7 @@ import { render, screen } from "@testing-library/react";
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AccuracyV3 } from "../_components/v3/AccuracyV3";
|
||||
import { DealsTickerV3 } from "../_components/v3/DealsTickerV3";
|
||||
import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3";
|
||||
import { HeroV3 } from "../_components/v3/HeroV3";
|
||||
import type { LandingStat, ShowcaseResponse } from "../public-api";
|
||||
|
|
@ -136,3 +137,31 @@ describe("витрина лэндинга v3 без данных", () => {
|
|||
expect(screen.queryByText(/По всем расчётам МЕРЫ/)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Лента висит НАД первым экраном, и её строки отобраны по полноте и свежести,
|
||||
* а не по величине ошибки, — крупный промах в ней штатен. Проверяется, что
|
||||
* рядом с промахом стоит его контекст и что оба числа сосчитаны ПО ПОКАЗАННЫМ
|
||||
* строкам: подпись с вписанными руками величинами тут же разошлась бы с лентой.
|
||||
*/
|
||||
describe("лента сделок: контекст разброса", () => {
|
||||
const tickerDeal = (err_pct: number) => ({
|
||||
...SHOWCASE.deals[0],
|
||||
err_pct,
|
||||
predicted_rub: Math.round(SHOWCASE.deals[0].fact_rub * (1 + err_pct / 100)),
|
||||
});
|
||||
|
||||
it("подпись печатает медиану и худшую ровно тех строк, что показаны", () => {
|
||||
render(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(-11.5), tickerDeal(75.7)]} />);
|
||||
const note = screen.getByText(/Медиана расхождения показанных строк/u);
|
||||
expect(note.textContent).toContain("11,5 %");
|
||||
expect(note.textContent).toContain("75,7 %");
|
||||
});
|
||||
|
||||
it("та же лента без худшей строки печатает ДРУГИЕ числа — они не константы", () => {
|
||||
render(<DealsTickerV3 deals={[tickerDeal(4), tickerDeal(-11.5)]} />);
|
||||
const note = screen.getByText(/Медиана расхождения показанных строк/u);
|
||||
expect(note.textContent).toContain("7,8 %");
|
||||
expect(note.textContent).not.toContain("75,7 %");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@
|
|||
* не из чего. Адреса тоже нет: улица и дом известны у 2.7% сделок, поэтому
|
||||
* объект описан тем, что есть — комнаты, площадь, район, этаж.
|
||||
*
|
||||
* ПОД лентой — подпись с разбросом ПОКАЗАННЫХ строк (`shownSpread`, та же
|
||||
* функция, что и под таблицей сверок): медиана модуля и худшая. Без неё первое,
|
||||
* что видит посетитель страницы про точность, — промах в семьдесят процентов
|
||||
* без единой цифры контекста, опровергающий её же заголовок. Прятать промахи
|
||||
* нельзя: строки отобраны по ПОЛНОТЕ и СВЕЖЕСТИ (`_sort_key` в
|
||||
* app/tasks/landing_showcase_deals.py), поэтому лечится контекстом, а не
|
||||
* отбором. Вывода («зато обычно точно») в подписи нет намеренно: он протух бы
|
||||
* на первом же пересчёте витрины, а два числа рядом не протухают.
|
||||
*
|
||||
* Список дублируется дважды подряд — стандартный приём бесшовного CSS-marquee
|
||||
* (анимация уводит ровно на −50%).
|
||||
*
|
||||
|
|
@ -24,34 +33,45 @@
|
|||
import type { ShowcaseDeal } from "../../public-api";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
|
||||
import { dealTitle, errPct, rub } from "./deal-view";
|
||||
import { absPct, dealTitle, errPct, rub, shownSpread } from "./deal-view";
|
||||
|
||||
export function DealsTickerV3({ deals }: { deals: readonly ShowcaseDeal[] }) {
|
||||
const items = [...deals, ...deals];
|
||||
// Разброс считается по `deals`, а не по `items`: `items` — та же двадцатка,
|
||||
// склеенная сама с собой ради бесшовного marquee. Медиана и худшая на ней
|
||||
// совпали бы, а `n` — нет, и подпись начала бы врать при первой же правке.
|
||||
const spread = shownSpread(deals);
|
||||
return (
|
||||
<div className={styles.tickerRoot}>
|
||||
<div className={styles.tickerBadge}>
|
||||
<span className={styles.tickerDot} aria-hidden="true" />
|
||||
<span className={styles.tickerBadgeText}>ПРОГНОЗ → ФАКТ</span>
|
||||
</div>
|
||||
<div className={styles.tickerTrack} aria-hidden="true">
|
||||
{items.map((deal, i) => (
|
||||
<div key={`${deal.deal_quarter}-${i}`} className={styles.tickerItem}>
|
||||
<span className={styles.tickerObj}>{dealTitle(deal)}</span>
|
||||
<span className={styles.tickerPair}>
|
||||
<span className={styles.tickerLabelMuted}>ПРОГНОЗ</span>
|
||||
<span className={styles.tickerSaid}>{rub(deal.predicted_rub)}</span>
|
||||
</span>
|
||||
<span className={styles.tickerArrow}>→</span>
|
||||
<span className={styles.tickerPair}>
|
||||
<span className={styles.tickerLabelAccent}>ДКП</span>
|
||||
<span className={styles.tickerFact}>{rub(deal.fact_rub)}</span>
|
||||
</span>
|
||||
<span className={styles.tickerErr}>{errPct(deal.err_pct)}</span>
|
||||
<span className={styles.tickerGain}>{deal.deal_quarter}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.tickerStrip}>
|
||||
<div className={styles.tickerBadge}>
|
||||
<span className={styles.tickerDot} aria-hidden="true" />
|
||||
<span className={styles.tickerBadgeText}>ПРОГНОЗ → ФАКТ</span>
|
||||
</div>
|
||||
<div className={styles.tickerTrack} aria-hidden="true">
|
||||
{items.map((deal, i) => (
|
||||
<div key={`${deal.deal_quarter}-${i}`} className={styles.tickerItem}>
|
||||
<span className={styles.tickerObj}>{dealTitle(deal)}</span>
|
||||
<span className={styles.tickerPair}>
|
||||
<span className={styles.tickerLabelMuted}>ПРОГНОЗ</span>
|
||||
<span className={styles.tickerSaid}>{rub(deal.predicted_rub)}</span>
|
||||
</span>
|
||||
<span className={styles.tickerArrow}>→</span>
|
||||
<span className={styles.tickerPair}>
|
||||
<span className={styles.tickerLabelAccent}>ДКП</span>
|
||||
<span className={styles.tickerFact}>{rub(deal.fact_rub)}</span>
|
||||
</span>
|
||||
<span className={styles.tickerErr}>{errPct(deal.err_pct)}</span>
|
||||
<span className={styles.tickerGain}>{deal.deal_quarter}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{spread && (
|
||||
<p className={styles.tickerNote}>
|
||||
{`Медиана расхождения показанных строк — ${absPct(spread.medianAbsPct)}, худшая — ${absPct(spread.worstAbsPct)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1399,9 +1399,22 @@
|
|||
|
||||
/* --- Лента «ПРОГНОЗ → ФАКТ» ---------------------------------------------- */
|
||||
.tickerRoot {
|
||||
background: var(--b2c-surface-dark);
|
||||
}
|
||||
/* Бегущая часть отделена от подписи: `overflow: hidden` и точка отсчёта для
|
||||
абсолютного бейджа нужны ТОЛЬКО ей, иначе бейдж растянулся бы на всю высоту
|
||||
вместе с подписью. */
|
||||
.tickerStrip {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--b2c-surface-dark);
|
||||
}
|
||||
.tickerNote {
|
||||
margin: 0;
|
||||
padding: 0 clamp(20px, 5vw, 56px) 10px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.tickerBadge {
|
||||
position: absolute;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue