generated from ttipo/project-base
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4d99c9127 | |||
| c6017fd0ed | |||
| 5caed15482 |
@@ -29,7 +29,7 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S
|
||||
| 테마·색 | `lib/core/theme/` | `AppColors` · `AppTheme` — 색·글자 크기를 화면에 새로 하드코딩하지 않는다 |
|
||||
| 한/영 문구 | `lib/l10n/*.arb` | `context.l10n.키`. 생성물 `lib/l10n/generated/` 는 git 제외 — `flutter pub get` 이 만든다 |
|
||||
| 한국어 줄바꿈 | `lib/core/utils/keep_all.dart` | 한국어 문단은 `keepAll()` 로 단어 단위 줄바꿈 (Flutter 기본은 글자 단위) |
|
||||
| 공공데이터 사진 | `lib/shared/widgets/public_image.dart` | 공공누리 Type3 은 자르지 않는다. 사진이 없으면 분류별 자리 표시. 출처 모델은 `shared/models/source_models.dart` |
|
||||
| 공공데이터 사진 | `lib/shared/widgets/public_image.dart` | 공공누리 Type3 은 자르지 않는다. 사진이 없으면 분류별 자리 표시. 그릴 크기로 줄여 푼다(`memCacheWidth`). 출처 모델은 `shared/models/source_models.dart` |
|
||||
|
||||
> **확장 시 따르는 규칙** (아래 "프로젝트 구조"는 목표 구조 — 기능 추가 시 점진적으로 채운다):
|
||||
> - **상태관리**: `features/user` 의 model→repository→provider 패턴을 복제. 위젯에서 `ref.watch(userProvider(id))`
|
||||
|
||||
+5
-1
@@ -60,5 +60,9 @@ lib/
|
||||
- 한국어 문단은 `keepAll()` 을 거쳐 단어 단위로 줄바꿈한다(Flutter 기본은 글자 단위).
|
||||
- 카드·배지 대신 흰 바탕 위 평면 목록과 구분선, 색 글자로 메타 정보를 보인다.
|
||||
- 공공누리 Type3 사진은 자르지 않는다(`PublicImage` 가 contain 으로 보인다 — 행사·장소 사진 모두 이 위젯을 거친다).
|
||||
- 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다.
|
||||
- 사진은 화면에 그릴 크기로 줄여 푼다(`PublicImage` → `decodeWidthFor`). 공연 포스터는 목록 썸네일도 원본(최대 9MB)이라 그대로 풀면 메모리를 많이 쓴다.
|
||||
- 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다
|
||||
(서버가 `trusted=true` 로 거른다 — 앱에서 거르면 상가가 빽빽한 도심에서 칸이 비었다).
|
||||
- `Semantics(excludeSemantics: true)` 로 버튼을 감싸면 안쪽 InkWell 의 누르기 동작까지 지워진다. Semantics 에도 `onTap` 을 같이 준다
|
||||
(빠뜨리면 TalkBack·VoiceOver 로 누를 수 없다 — `widget_test.dart` 의 화면 읽기 테스트가 잡는다).
|
||||
- 실패 문구는 에러 사전(`error_lexicon.g.dart`)에서만 가져온다 — `errorMessageOf()`.
|
||||
|
||||
@@ -70,11 +70,13 @@ class _TabItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = selected ? AppColors.brand : AppColors.textTertiary;
|
||||
// excludeSemantics 는 아래 InkResponse 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: label,
|
||||
excludeSemantics: true,
|
||||
onTap: onTap,
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
radius: 36,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
/// cover 로 채울 때 가로가 긴 사진(최대 2:1)도 세로를 채우도록 세로 길이의 이만큼까지 가로를 둔다
|
||||
const double kCoverAspectAllowance = 2;
|
||||
|
||||
/// 사진을 화면에 그릴 크기에 맞춰 줄여 풀 가로 픽셀 수 (순수 함수).
|
||||
///
|
||||
/// 공연 포스터는 목록 썸네일도 원본(수 MB, 수천 픽셀)이라 그대로 풀면 사진 한 장에 수십 MB 를 쓴다.
|
||||
/// 가로만 정하면 비율은 유지되고, 원본보다 크게 늘리지는 않는다.
|
||||
/// - contain: 상자 가로만큼이면 된다
|
||||
/// - cover: 가로가 긴 사진도 세로를 채워야 하므로 max(가로, 세로 × [kCoverAspectAllowance])
|
||||
/// 크기를 모르면 null(원본 그대로 푼다).
|
||||
int? decodeWidthFor({
|
||||
required double? boxWidth,
|
||||
required double? boxHeight,
|
||||
required BoxFit fit,
|
||||
required double devicePixelRatio,
|
||||
}) {
|
||||
final width = boxWidth != null && boxWidth.isFinite && boxWidth > 0
|
||||
? boxWidth
|
||||
: null;
|
||||
final height = boxHeight != null && boxHeight.isFinite && boxHeight > 0
|
||||
? boxHeight
|
||||
: null;
|
||||
final logical = switch ((width, height)) {
|
||||
(final w?, final h?) =>
|
||||
fit == BoxFit.cover ? math.max(w, h * kCoverAspectAllowance) : w,
|
||||
(final w?, null) => w,
|
||||
(null, final h?) => h * kCoverAspectAllowance,
|
||||
(null, null) => null,
|
||||
};
|
||||
return logical == null ? null : (logical * devicePixelRatio).ceil();
|
||||
}
|
||||
@@ -132,17 +132,21 @@ class _BaseLocationButton extends ConsumerWidget {
|
||||
final label = fellBack
|
||||
? kFallbackNeighborhood.label(l10n)
|
||||
: choice.label(l10n);
|
||||
Future<void> pickBase() async {
|
||||
final picked = await showRegionSheet(context, selected: choice);
|
||||
if (picked != null && context.mounted) {
|
||||
await applyBaseLocation(context, ref, picked);
|
||||
}
|
||||
}
|
||||
|
||||
// excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: l10n.baseLocationButtonLabel(label),
|
||||
excludeSemantics: true,
|
||||
onTap: pickBase,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showRegionSheet(context, selected: choice);
|
||||
if (picked != null && context.mounted) {
|
||||
await applyBaseLocation(context, ref, picked);
|
||||
}
|
||||
},
|
||||
onTap: pickBase,
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: Row(
|
||||
|
||||
@@ -115,10 +115,12 @@ class _CategoryCell extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = shortcut.label(context.l10n);
|
||||
// excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: label,
|
||||
excludeSemantics: true,
|
||||
onTap: onTap,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
|
||||
@@ -14,16 +14,19 @@ class HomeSearchBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hint = context.l10n.searchHint;
|
||||
void openSearch() => context.push(AppRoutes.placeSearch(focus: true));
|
||||
// excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: hint,
|
||||
excludeSemantics: true,
|
||||
onTap: openSearch,
|
||||
child: Material(
|
||||
color: AppColors.inputFill,
|
||||
borderRadius: BorderRadius.circular(kRadius),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(kRadius),
|
||||
onTap: () => context.push(AppRoutes.placeSearch(focus: true)),
|
||||
onTap: openSearch,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
|
||||
@@ -15,10 +15,12 @@ class PlaceRepository {
|
||||
|
||||
/// 장소 검색. [near] 를 주면 [radiusM] 안을 거리순으로, 없으면 이름순으로 준다.
|
||||
/// [categories] 가 비면 전체 분류, [petOnly] 면 반려동물 동반(전 구역·일부 구역) 장소만.
|
||||
/// [trustedOnly] 면 관광공사 장소와 운영자가 확인한 장소만(확인 안 된 상가정보 매장·추천 안 함 제외).
|
||||
Future<PlacePage> search({
|
||||
String? query,
|
||||
List<PlaceCategory> categories = const [],
|
||||
bool petOnly = false,
|
||||
bool trustedOnly = false,
|
||||
GeoPoint? near,
|
||||
int? radiusM,
|
||||
int limit = 20,
|
||||
@@ -31,6 +33,7 @@ class PlaceRepository {
|
||||
if (categories.isNotEmpty)
|
||||
'category': categories.map((c) => c.name).join(','),
|
||||
if (petOnly) 'pet': 'true',
|
||||
if (trustedOnly) 'trusted': 'true',
|
||||
'lat': ?near?.lat,
|
||||
'lng': ?near?.lng,
|
||||
'radius': ?radiusM,
|
||||
|
||||
@@ -427,17 +427,20 @@ class FilterToggleChip extends StatelessWidget {
|
||||
color: selected ? AppColors.chipBorderSelected : AppColors.chipBorder,
|
||||
),
|
||||
);
|
||||
void toggle() => onChanged(!selected);
|
||||
// excludeSemantics 는 InkWell 의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
toggled: selected,
|
||||
button: true,
|
||||
excludeSemantics: true,
|
||||
label: label,
|
||||
onTap: toggle,
|
||||
child: Material(
|
||||
color: selected ? AppColors.brandSoft : AppColors.background,
|
||||
shape: shape,
|
||||
child: InkWell(
|
||||
customBorder: shape,
|
||||
onTap: () => onChanged(!selected),
|
||||
onTap: toggle,
|
||||
child: Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
|
||||
@@ -18,9 +18,8 @@ const int kHomePetPlaceLimit = 3;
|
||||
const int kNearbyEventRadiusM = 5000;
|
||||
const int kNearbyEventLimit = 3;
|
||||
|
||||
/// 장소 상세 '함께 가기 좋은 곳' — 반경 안에서 받아 고른 뒤 이만큼 보인다
|
||||
/// 장소 상세 '함께 가기 좋은 곳' — 반경 안에서 이만큼 보인다
|
||||
const int kNearbyPlaceRadiusM = 1500;
|
||||
const int kNearbyPlaceFetchLimit = 30;
|
||||
const int kNearbyPlaceLimit = 6;
|
||||
|
||||
/// 장소 상세
|
||||
@@ -66,21 +65,20 @@ typedef NearbyPlaceKey = ({String id, double lat, double lng});
|
||||
///
|
||||
/// 확인 안 된 상가정보 매장은 고르지 않는다(관광공사 장소 또는 운영자가 확인한 장소만).
|
||||
/// 폐업·분위기를 검증할 수 없는 곳을 추천처럼 보이지 않으려는 것이다(2026-11-02 합의).
|
||||
/// 서버가 거른다(`trusted`) — 앱이 가까운 몇 곳을 받아 거르면 도심처럼 상가가 빽빽한 곳에서는
|
||||
/// 받은 곳이 모두 상가라 칸이 비었다(2026-11-04 회귀 점검). 자기 자신이 섞여 오므로 하나 더 받는다.
|
||||
final nearbyPlacesProvider =
|
||||
FutureProvider.family<List<PlaceItem>, NearbyPlaceKey>((ref, key) async {
|
||||
final page = await ref
|
||||
.watch(placeRepositoryProvider)
|
||||
.search(
|
||||
trustedOnly: true,
|
||||
near: (lat: key.lat, lng: key.lng),
|
||||
radiusM: kNearbyPlaceRadiusM,
|
||||
limit: kNearbyPlaceFetchLimit,
|
||||
limit: kNearbyPlaceLimit + 1,
|
||||
);
|
||||
return page.items
|
||||
.where(
|
||||
(p) =>
|
||||
p.id != key.id &&
|
||||
(p.source == PlaceSource.tour || p.verifiedAt != null),
|
||||
)
|
||||
.where((p) => p.id != key.id)
|
||||
.take(kNearbyPlaceLimit)
|
||||
.toList();
|
||||
});
|
||||
|
||||
@@ -16,12 +16,15 @@ class LanguageButton extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = context.l10n;
|
||||
void openSheet() => _openSheet(context, ref);
|
||||
// excludeSemantics 는 버튼의 누르기 동작까지 지운다 — 화면 읽기 프로그램이 누를 수 있게 onTap 을 함께 준다
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: l10n.languageButtonLabel(l10n.languageName),
|
||||
excludeSemantics: true,
|
||||
onTap: openSheet,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _openSheet(context, ref),
|
||||
onPressed: openSheet,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: Size(44, compact ? 44 : 40),
|
||||
padding: EdgeInsets.only(left: 12, right: compact ? 12 : 10),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:wandergo/core/theme/app_colors.dart';
|
||||
import 'package:wandergo/core/theme/app_theme.dart';
|
||||
import 'package:wandergo/core/utils/image_decode.dart';
|
||||
import 'package:wandergo/shared/models/source_models.dart';
|
||||
|
||||
/// 사진이 없거나 못 불러올 때의 자리 표시 — 분류별 배경색·아이콘
|
||||
@@ -49,6 +50,8 @@ class PublicImage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
final imageUrl = url;
|
||||
final boxFit =
|
||||
fit ?? (license == ImageLicense.type3 ? BoxFit.contain : BoxFit.cover);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: SizedBox(
|
||||
@@ -58,16 +61,21 @@ class PublicImage extends StatelessWidget {
|
||||
? fallback
|
||||
: ColoredBox(
|
||||
color: AppColors.placeholder,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit:
|
||||
fit ??
|
||||
(license == ImageLicense.type3
|
||||
? BoxFit.contain
|
||||
: BoxFit.cover),
|
||||
errorWidget: (_, _, _) => fallback,
|
||||
// 크기를 정하지 않은 쪽(상세 위쪽 사진의 가로 등)은 실제로 받은 폭으로 계산한다
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: boxFit,
|
||||
memCacheWidth: decodeWidthFor(
|
||||
boxWidth: width ?? constraints.maxWidth,
|
||||
boxHeight: height ?? constraints.maxHeight,
|
||||
fit: boxFit,
|
||||
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
|
||||
),
|
||||
errorWidget: (_, _, _) => fallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wandergo/core/utils/image_decode.dart';
|
||||
|
||||
void main() {
|
||||
group('decodeWidthFor', () {
|
||||
test('cover 는 가로가 긴 사진도 세로를 채우도록 세로 × 2 까지 가로를 둔다', () {
|
||||
// 목록 줄 64×64, 화면 배율 3 → max(64, 128) × 3
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 64,
|
||||
boxHeight: 64,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 3,
|
||||
),
|
||||
384,
|
||||
);
|
||||
// 함께 가기 카드 148×104 → max(148, 208) × 3
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 148,
|
||||
boxHeight: 104,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 3,
|
||||
),
|
||||
624,
|
||||
);
|
||||
});
|
||||
|
||||
test('contain(공공누리 Type3)은 상자 가로만큼이면 된다', () {
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 148,
|
||||
boxHeight: 104,
|
||||
fit: BoxFit.contain,
|
||||
devicePixelRatio: 2.625,
|
||||
),
|
||||
389,
|
||||
);
|
||||
});
|
||||
|
||||
test('가로를 모르면 세로로, 둘 다 모르면 null(원본 그대로)', () {
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: double.infinity,
|
||||
boxHeight: 240,
|
||||
fit: BoxFit.contain,
|
||||
devicePixelRatio: 2,
|
||||
),
|
||||
960,
|
||||
);
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: null,
|
||||
boxHeight: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 2,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:wandergo/features/location/data/location_service.dart';
|
||||
import 'package:wandergo/features/location/domain/base_location.dart';
|
||||
import 'package:wandergo/features/place/data/models/place_models.dart';
|
||||
import 'package:wandergo/features/place/data/repositories/place_repository.dart';
|
||||
import 'package:wandergo/features/place/presentation/providers/place_providers.dart';
|
||||
|
||||
/// 2026-11-04(수) 한국 정오 — 이번 주말은 11-07~11-08
|
||||
final DateTime _now = DateTime.utc(2026, 11, 4, 3);
|
||||
@@ -113,7 +114,15 @@ class _FakePlaceRepository implements PlaceRepository {
|
||||
|
||||
final List<PlaceItem> places;
|
||||
final List<PlaceItem> petPlaces;
|
||||
final List<({String? query, List<PlaceCategory> categories, bool petOnly})>
|
||||
final List<
|
||||
({
|
||||
String? query,
|
||||
List<PlaceCategory> categories,
|
||||
bool petOnly,
|
||||
bool trustedOnly,
|
||||
int limit,
|
||||
})
|
||||
>
|
||||
calls = [];
|
||||
|
||||
@override
|
||||
@@ -121,13 +130,29 @@ class _FakePlaceRepository implements PlaceRepository {
|
||||
String? query,
|
||||
List<PlaceCategory> categories = const [],
|
||||
bool petOnly = false,
|
||||
bool trustedOnly = false,
|
||||
GeoPoint? near,
|
||||
int? radiusM,
|
||||
int limit = 20,
|
||||
String? cursor,
|
||||
}) async {
|
||||
calls.add((query: query, categories: categories, petOnly: petOnly));
|
||||
final items = petOnly ? petPlaces : places;
|
||||
calls.add((
|
||||
query: query,
|
||||
categories: categories,
|
||||
petOnly: petOnly,
|
||||
trustedOnly: trustedOnly,
|
||||
limit: limit,
|
||||
));
|
||||
// 서버처럼 trusted 면 관광공사·운영자 확인 장소만 주고, limit 만큼 자른다
|
||||
final items = (petOnly ? petPlaces : places)
|
||||
.where(
|
||||
(p) =>
|
||||
!trustedOnly ||
|
||||
p.source == PlaceSource.tour ||
|
||||
p.verifiedAt != null,
|
||||
)
|
||||
.take(limit)
|
||||
.toList();
|
||||
return PlacePage(items: items, hasNext: false, total: items.length);
|
||||
}
|
||||
|
||||
@@ -389,4 +414,65 @@ void main() {
|
||||
expect(find.text('Need help? Call 1330'), findsOneWidget);
|
||||
expect(_text('Hours not confirmed · check before you go'), findsOneWidget);
|
||||
});
|
||||
|
||||
// 2026-11-04 회귀 점검: 앱이 가까운 30곳을 받아 거르면 도심에서는 모두 상가라 칸이 비었다
|
||||
testWidgets('함께 가기 좋은 곳은 관광공사·확인 장소만 서버에 요청해, 상가가 더 가까워도 채워진다', (
|
||||
tester,
|
||||
) async {
|
||||
await _useTallScreen(tester);
|
||||
final alley = _place(
|
||||
'p-alley',
|
||||
'대구근대골목',
|
||||
category: PlaceCategory.attraction,
|
||||
distanceM: 320,
|
||||
);
|
||||
final places = _FakePlaceRepository(
|
||||
places: [_storeCafe, _petPark, alley],
|
||||
petPlaces: [_petPark],
|
||||
);
|
||||
await _pumpApp(tester, prefs: {'onboarding_done': true}, places: places);
|
||||
|
||||
await tester.tap(find.text('경상감영공원'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final nearby = places.calls.where((c) => c.trustedOnly).toList();
|
||||
expect(nearby, hasLength(1));
|
||||
// 자기 자신이 섞여 오므로 보일 수보다 하나 더 받는다
|
||||
expect(nearby.single.limit, kNearbyPlaceLimit + 1);
|
||||
expect(find.text('함께 가기 좋은 곳'), findsOneWidget);
|
||||
expect(find.text('대구근대골목'), findsOneWidget);
|
||||
expect(find.text('마실커피 동성로점'), findsNothing);
|
||||
});
|
||||
|
||||
// 2026-11-04 회귀 점검: excludeSemantics 가 누르기 동작까지 지워 TalkBack 으로 누를 수 없었다
|
||||
testWidgets('홈·검색의 버튼은 화면 읽기 프로그램으로도 누를 수 있다', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
await _pumpApp(
|
||||
tester,
|
||||
prefs: {'onboarding_done': true},
|
||||
places: _FakePlaceRepository(places: [_storeCafe]),
|
||||
);
|
||||
|
||||
for (final label in <Pattern>[
|
||||
'홈',
|
||||
'관광지',
|
||||
'장소 이름으로 검색',
|
||||
RegExp('^언어 변경'),
|
||||
RegExp('^기준 위치 변경'),
|
||||
]) {
|
||||
expect(
|
||||
tester.getSemantics(find.bySemanticsLabel(label).first),
|
||||
isSemantics(isButton: true, hasTapAction: true),
|
||||
reason: '$label',
|
||||
);
|
||||
}
|
||||
|
||||
await tester.tap(find.text('관광지'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSemantics(find.bySemanticsLabel('반려견 동반')),
|
||||
isSemantics(isButton: true, hasTapAction: true),
|
||||
);
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user