feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -49,26 +49,26 @@ HoldingOut holding({
|
||||
}
|
||||
|
||||
SummaryOut summary() => SummaryOut(
|
||||
asOf: DateTime(2026, 9, 18),
|
||||
cashRub: '5700',
|
||||
computedAt: DateTime.utc(2026, 9, 18, 10),
|
||||
holdingCount: 2,
|
||||
incomeRub: '700',
|
||||
investedNetRub: '20000',
|
||||
marketValueRub: '11000',
|
||||
pnlTotalRub: null,
|
||||
realizedPnlRub: '0',
|
||||
returns: const [],
|
||||
scope: 'all',
|
||||
staleCount: 0,
|
||||
totalRub: '16700',
|
||||
unpricedCount: 1,
|
||||
);
|
||||
asOf: DateTime(2026, 9, 18),
|
||||
cashRub: '5700',
|
||||
computedAt: DateTime.utc(2026, 9, 18, 10),
|
||||
holdingCount: 2,
|
||||
incomeRub: '700',
|
||||
investedNetRub: '20000',
|
||||
marketValueRub: '11000',
|
||||
pnlTotalRub: null,
|
||||
realizedPnlRub: '0',
|
||||
returns: const [],
|
||||
scope: 'all',
|
||||
staleCount: 0,
|
||||
totalRub: '16700',
|
||||
unpricedCount: 1,
|
||||
);
|
||||
|
||||
Widget _wrap(Widget child, List<Override> overrides) => ProviderScope(
|
||||
overrides: overrides,
|
||||
child: MaterialApp(home: Scaffold(body: child)),
|
||||
);
|
||||
overrides: overrides,
|
||||
child: MaterialApp(home: Scaffold(body: child)),
|
||||
);
|
||||
|
||||
/// The tabs are long scrolling lists and a ListView only builds what fits, so the default
|
||||
/// 800x600 surface would leave the holdings table unbuilt and every `find` on it empty.
|
||||
@@ -81,22 +81,30 @@ Future<void> pumpTall(WidgetTester tester, Widget widget) async {
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Позиции show an unpriced holding as «—», never as 0 ₽', (tester) async {
|
||||
await pumpTall(tester, _wrap(
|
||||
const HoldingsTab(),
|
||||
[
|
||||
testWidgets('Позиции show an unpriced holding as «—», never as 0 ₽', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTall(
|
||||
tester,
|
||||
_wrap(const HoldingsTab(), [
|
||||
portfolioSummaryProvider.overrideWith((ref) => Cached(summary())),
|
||||
valueSeriesProvider.overrideWith((ref) => const Cached(<ValueDay>[])),
|
||||
portfolioReturnsProvider.overrideWith((ref) => const Cached(<ReturnsOut>[])),
|
||||
holdingsProvider.overrideWith((ref) => Cached([
|
||||
holding(id: 1, ticker: 'GAZP', valueRub: '11000', weight: '1'),
|
||||
holding(id: 2, ticker: 'SIBN6P4', priceStatus: 'missing'),
|
||||
])),
|
||||
portfolioReturnsProvider.overrideWith(
|
||||
(ref) => const Cached(<ReturnsOut>[]),
|
||||
),
|
||||
holdingsProvider.overrideWith(
|
||||
(ref) => Cached([
|
||||
holding(id: 1, ticker: 'GAZP', valueRub: '11000', weight: '1'),
|
||||
holding(id: 2, ticker: 'SIBN6P4', priceStatus: 'missing'),
|
||||
]),
|
||||
),
|
||||
// HoldingsTab embeds BenchmarksCard, which watches this too — leaving it out would
|
||||
// fall through to the real apiProvider (see docs/ai/offline-cache.md).
|
||||
benchmarkRowsProvider.overrideWith((ref) => const Cached(<BenchmarkRow>[])),
|
||||
],
|
||||
));
|
||||
benchmarkRowsProvider.overrideWith(
|
||||
(ref) => const Cached(<BenchmarkRow>[]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(find.text('GAZP'), findsOneWidget);
|
||||
expect(find.text('SIBN6P4'), findsOneWidget);
|
||||
@@ -107,44 +115,54 @@ void main() {
|
||||
expect(find.text(MoneyText.format('0', 'RUB')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Позиции show the total profit as «—» while a price is missing', (tester) async {
|
||||
await pumpTall(tester, _wrap(
|
||||
const HoldingsTab(),
|
||||
[
|
||||
testWidgets('Позиции show the total profit as «—» while a price is missing', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTall(
|
||||
tester,
|
||||
_wrap(const HoldingsTab(), [
|
||||
portfolioSummaryProvider.overrideWith((ref) => Cached(summary())),
|
||||
valueSeriesProvider.overrideWith((ref) => const Cached(<ValueDay>[])),
|
||||
portfolioReturnsProvider.overrideWith((ref) => const Cached(<ReturnsOut>[])),
|
||||
portfolioReturnsProvider.overrideWith(
|
||||
(ref) => const Cached(<ReturnsOut>[]),
|
||||
),
|
||||
holdingsProvider.overrideWith((ref) => const Cached(<HoldingOut>[])),
|
||||
benchmarkRowsProvider.overrideWith((ref) => const Cached(<BenchmarkRow>[])),
|
||||
],
|
||||
));
|
||||
benchmarkRowsProvider.overrideWith(
|
||||
(ref) => const Cached(<BenchmarkRow>[]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(find.text('Прибыль'), findsOneWidget);
|
||||
expect(find.text('часть позиций без цены'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Аллокация labels the cash and unknown buckets in Russian', (tester) async {
|
||||
await pumpTall(tester, _wrap(
|
||||
const AllocationTab(),
|
||||
[
|
||||
allocationProvider.overrideWith((ref) => Cached([
|
||||
AllocationBucket(
|
||||
bucket: 'share',
|
||||
dimension: AllocationDimension.assetClass,
|
||||
holdingCount: 1,
|
||||
valueRub: '11000',
|
||||
weight: '0.6587',
|
||||
),
|
||||
AllocationBucket(
|
||||
bucket: 'cash',
|
||||
dimension: AllocationDimension.assetClass,
|
||||
holdingCount: 0,
|
||||
valueRub: '5700',
|
||||
weight: '0.3413',
|
||||
),
|
||||
])),
|
||||
],
|
||||
));
|
||||
testWidgets('Аллокация labels the cash and unknown buckets in Russian', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTall(
|
||||
tester,
|
||||
_wrap(const AllocationTab(), [
|
||||
allocationProvider.overrideWith(
|
||||
(ref) => Cached([
|
||||
AllocationBucket(
|
||||
bucket: 'share',
|
||||
dimension: AllocationDimension.assetClass,
|
||||
holdingCount: 1,
|
||||
valueRub: '11000',
|
||||
weight: '0.6587',
|
||||
),
|
||||
AllocationBucket(
|
||||
bucket: 'cash',
|
||||
dimension: AllocationDimension.assetClass,
|
||||
holdingCount: 0,
|
||||
valueRub: '5700',
|
||||
weight: '0.3413',
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(find.text('Класс актива'), findsOneWidget);
|
||||
expect(find.text('Акции'), findsOneWidget);
|
||||
@@ -168,6 +186,9 @@ void main() {
|
||||
test('bucketLabel falls back to the source value it does not know', () {
|
||||
expect(bucketLabel(AllocationDimension.country, 'RU'), 'Россия');
|
||||
expect(bucketLabel(AllocationDimension.country, 'unknown'), 'Не указано');
|
||||
expect(bucketLabel(AllocationDimension.sector, 'oil_and_gas'), 'oil_and_gas');
|
||||
expect(
|
||||
bucketLabel(AllocationDimension.sector, 'oil_and_gas'),
|
||||
'oil_and_gas',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user