1
0
forked from baron/baron-sso

code check 오류 수정

This commit is contained in:
2026-03-23 15:36:00 +09:00
parent 3c54c46898
commit e98ab39dfe
11 changed files with 155 additions and 104 deletions

View File

@@ -117,6 +117,10 @@ endif
.PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-userfront-e2e-tests .PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-userfront-e2e-tests
CODE_CHECK_TEST_JOBS ?= 1
PLAYWRIGHT_WORKERS ?= 1
FLUTTER_TEST_CONCURRENCY ?= 1
code-check: code-check-lint code-check-test-jobs code-check: code-check-lint code-check-test-jobs
@echo "code-check complete." @echo "code-check complete."
@@ -124,7 +128,7 @@ code-check-lint: code-check-i18n code-check-front-lint code-check-go-lint code-c
code-check-test-jobs: code-check-test-jobs:
@echo "==> run CI-equivalent test jobs (parallel)" @echo "==> run CI-equivalent test jobs (parallel)"
@$(MAKE) --no-print-directory -j5 --output-sync=target \ @$(MAKE) --no-print-directory -j$(CODE_CHECK_TEST_JOBS) --output-sync=target \
code-check-backend-tests \ code-check-backend-tests \
code-check-userfront-tests \ code-check-userfront-tests \
code-check-userfront-e2e-tests \ code-check-userfront-e2e-tests \
@@ -203,11 +207,11 @@ code-check-userfront-tests:
rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \ rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \
fi; \ fi; \
cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh; \ cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh; \
cd "$$tmp_dir/userfront" && flutter test cd "$$tmp_dir/userfront" && flutter test --concurrency=$(FLUTTER_TEST_CONCURRENCY)
code-check-adminfront-tests: code-check-adminfront-tests:
@echo "==> adminfront tests" @echo "==> adminfront tests"
./scripts/run_adminfront_ci_tests.sh adminfront-tests PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) ./scripts/run_adminfront_ci_tests.sh adminfront-tests
code-check-devfront-tests: code-check-devfront-tests:
@echo "==> devfront tests" @echo "==> devfront tests"
@@ -219,7 +223,7 @@ code-check-devfront-tests:
(cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ (cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \
fi; \ fi; \
if [ $$status -eq 0 ]; then \ if [ $$status -eq 0 ]; then \
(cd devfront && npm test) || status=$$?; \ (cd devfront && PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \
fi; \ fi; \
[ -d devfront/playwright-report ] && cp -R devfront/playwright-report reports/devfront/ || true; \ [ -d devfront/playwright-report ] && cp -R devfront/playwright-report reports/devfront/ || true; \
[ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \ [ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \
@@ -267,7 +271,7 @@ code-check-userfront-e2e-tests:
if [ $$status -eq 0 ]; then \ if [ $$status -eq 0 ]; then \
port="$$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"; \ port="$$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"; \
echo "==> userfront-e2e using PORT=$$port"; \ echo "==> userfront-e2e using PORT=$$port"; \
(cd "$$tmp_dir/userfront-e2e" && PORT=$$port npm test) || status=$$?; \ (cd "$$tmp_dir/userfront-e2e" && PORT=$$port PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \
fi; \ fi; \
[ -d "$$tmp_dir/userfront-e2e/playwright-report" ] && cp -R "$$tmp_dir/userfront-e2e/playwright-report" reports/userfront-e2e/ || true; \ [ -d "$$tmp_dir/userfront-e2e/playwright-report" ] && cp -R "$$tmp_dir/userfront-e2e/playwright-report" reports/userfront-e2e/ || true; \
[ -d "$$tmp_dir/userfront-e2e/test-results" ] && cp -R "$$tmp_dir/userfront-e2e/test-results" reports/userfront-e2e/ || true; \ [ -d "$$tmp_dir/userfront-e2e/test-results" ] && cp -R "$$tmp_dir/userfront-e2e/test-results" reports/userfront-e2e/ || true; \

View File

@@ -1,5 +1,9 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
@@ -24,7 +28,7 @@ export default defineConfig({
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: configuredWorkers ?? (process.env.CI ? 1 : undefined),
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

View File

@@ -1,5 +1,9 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
@@ -20,7 +24,7 @@ export default defineConfig({
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: configuredWorkers ?? (process.env.CI ? 1 : undefined),
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

View File

@@ -4,13 +4,16 @@ const port = Number.parseInt(process.env.PORT ?? '4173', 10);
const defaultBaseUrl = `http://127.0.0.1:${port}`; const defaultBaseUrl = `http://127.0.0.1:${port}`;
const baseURL = process.env.BASE_URL ?? defaultBaseUrl; const baseURL = process.env.BASE_URL ?? defaultBaseUrl;
const reuseExistingServer = !process.env.CI; const reuseExistingServer = !process.env.CI;
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
export default defineConfig({ export default defineConfig({
testDir: './tests', testDir: './tests',
fullyParallel: false, fullyParallel: false,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined, workers: configuredWorkers ?? (process.env.CI ? 1 : undefined),
reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html', reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html',
use: { use: {
baseURL, baseURL,

View File

@@ -47,6 +47,11 @@ async function blurDepartmentEditor(page: Page): Promise<void> {
await page.waitForTimeout(250); await page.waitForTimeout(250);
} }
async function submitDepartmentEditor(page: Page): Promise<void> {
await page.keyboard.press('Enter');
await page.waitForTimeout(250);
}
async function mockProfileApis(page: Page, state: ProfileState): Promise<void> { async function mockProfileApis(page: Page, state: ProfileState): Promise<void> {
await page.route('**/api/v1/**', async (route: Route) => { await page.route('**/api/v1/**', async (route: Route) => {
const request = route.request(); const request = route.request();
@@ -155,7 +160,7 @@ test.describe('UserFront WASM profile department editing', () => {
await page.unroute('**/api/v1/**'); await page.unroute('**/api/v1/**');
}); });
test('소속 수정 후 포커스 아웃하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({
page, page,
}) => { }) => {
const state: ProfileState = { const state: ProfileState = {
@@ -170,7 +175,7 @@ test.describe('UserFront WASM profile department editing', () => {
await openDepartmentEditor(page); await openDepartmentEditor(page);
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated'); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated');
await blurDepartmentEditor(page); await submitDepartmentEditor(page);
await expect.poll(() => state.putBodies.length).toBe(1); await expect.poll(() => state.putBodies.length).toBe(1);
expect(state.putBodies[0]?.department).toBe('QA-Updated'); expect(state.putBodies[0]?.department).toBe('QA-Updated');
@@ -248,7 +253,7 @@ test.describe('UserFront WASM profile department editing', () => {
expect(state.department).toBe('QA'); expect(state.department).toBe('QA');
}); });
test('소속을 수정한 뒤 새로고침 후 다시 수정해도 저장 요청이 누락되지 않는다', async ({ page }) => { test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => {
const state: ProfileState = { const state: ProfileState = {
department: 'QA', department: 'QA',
getMeCount: 0, getMeCount: 0,
@@ -261,7 +266,7 @@ test.describe('UserFront WASM profile department editing', () => {
await openDepartmentEditor(page); await openDepartmentEditor(page);
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1'); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1');
await blurDepartmentEditor(page); await submitDepartmentEditor(page);
await expect.poll(() => state.putBodies.length).toBe(1); await expect.poll(() => state.putBodies.length).toBe(1);
await page.reload(); await page.reload();
@@ -270,7 +275,7 @@ test.describe('UserFront WASM profile department editing', () => {
await openDepartmentEditor(page); await openDepartmentEditor(page);
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2'); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2');
await blurDepartmentEditor(page); await submitDepartmentEditor(page);
await expect.poll(() => state.putBodies.length).toBe(2); await expect.poll(() => state.putBodies.length).toBe(2);
expect(state.putBodies[0]?.department).toBe('QA-1'); expect(state.putBodies[0]?.department).toBe('QA-1');

File diff suppressed because one or more lines are too long

View File

@@ -277,6 +277,11 @@ privacy_full = "개인정보 수집 및 이용 동의 전문..."
tos_full = "서비스 이용약관 전문..." tos_full = "서비스 이용약관 전문..."
[msg.userfront.signup.agreement] [msg.userfront.signup.agreement]
all_hint = "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다."
description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요."
privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다."
progress = "필수 약관 {total}개 중 {count}개 동의 완료"
tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다."
title = "서비스 이용을 위해\n약관에 동의해주세요" title = "서비스 이용을 위해\n약관에 동의해주세요"
[msg.userfront.signup.auth] [msg.userfront.signup.auth]
@@ -583,6 +588,7 @@ title = "회원가입"
[ui.userfront.signup.agreement] [ui.userfront.signup.agreement]
all = "모두 동의합니다" all = "모두 동의합니다"
privacy_title = "개인정보 수집 및 이용 동의 (필수)" privacy_title = "개인정보 수집 및 이용 동의 (필수)"
required = "필수"
tos_title = "바론 소프트웨어 이용약관 (필수)" tos_title = "바론 소프트웨어 이용약관 (필수)"
[ui.userfront.signup.auth] [ui.userfront.signup.auth]
@@ -616,4 +622,3 @@ verify = "본인인증"
[ui.userfront.signup.success] [ui.userfront.signup.success]
action = "로그인하기" action = "로그인하기"

View File

@@ -277,6 +277,11 @@ privacy_full = ""
tos_full = "" tos_full = ""
[msg.userfront.signup.agreement] [msg.userfront.signup.agreement]
all_hint = ""
description = ""
privacy_summary = ""
progress = ""
tos_summary = ""
title = "" title = ""
[msg.userfront.signup.auth] [msg.userfront.signup.auth]
@@ -583,6 +588,7 @@ title = ""
[ui.userfront.signup.agreement] [ui.userfront.signup.agreement]
all = "" all = ""
privacy_title = "" privacy_title = ""
required = ""
tos_title = "" tos_title = ""
[ui.userfront.signup.auth] [ui.userfront.signup.auth]
@@ -616,4 +622,3 @@ verify = ""
[ui.userfront.signup.success] [ui.userfront.signup.success]
action = "" action = ""

View File

@@ -238,8 +238,8 @@ class _ConsentScreenState extends State<ConsentScreen> {
final clientName = (clientRawName != null && clientRawName.isNotEmpty) final clientName = (clientRawName != null && clientRawName.isNotEmpty)
? clientRawName ? clientRawName
: (clientId != '-' : (clientId != '-'
? clientId ? clientId
: tr('msg.userfront.consent.client_unknown')); : tr('msg.userfront.consent.client_unknown'));
final clientLogo = _consentInfo?['client']?['logo_uri']; final clientLogo = _consentInfo?['client']?['logo_uri'];
final requestedScopes = final requestedScopes =
(_consentInfo?['requested_scope'] as List<dynamic>?)?.cast<String>() ?? (_consentInfo?['requested_scope'] as List<dynamic>?)?.cast<String>() ??

View File

@@ -1494,8 +1494,7 @@ class _SignupScreenState extends State<SignupScreen> {
labelText: tr( labelText: tr(
'ui.userfront.signup.profile.company', 'ui.userfront.signup.profile.company',
), ),
border: border: const OutlineInputBorder(),
const OutlineInputBorder(),
), ),
items: [ items: [
DropdownMenuItem( DropdownMenuItem(
@@ -1557,7 +1556,9 @@ class _SignupScreenState extends State<SignupScreen> {
_buildProfileFieldGroup( _buildProfileFieldGroup(
title: _affiliationType == 'AFFILIATE' title: _affiliationType == 'AFFILIATE'
? tr('ui.userfront.signup.profile.department') ? tr('ui.userfront.signup.profile.department')
: tr('ui.userfront.signup.profile.department_optional'), : tr(
'ui.userfront.signup.profile.department_optional',
),
description: _affiliationType == 'AFFILIATE' description: _affiliationType == 'AFFILIATE'
? '가족사 사용자는 부서명을 입력해주세요.' ? '가족사 사용자는 부서명을 입력해주세요.'
: '선택 입력 항목입니다.', : '선택 입력 항목입니다.',
@@ -1677,10 +1678,7 @@ class _SignupScreenState extends State<SignupScreen> {
], ],
), ),
), ),
if (trailing != null) ...[ if (trailing != null) ...[const SizedBox(width: 12), trailing],
const SizedBox(width: 12),
trailing,
],
], ],
), ),
SizedBox(height: isDesktop ? 18 : 14), SizedBox(height: isDesktop ? 18 : 14),
@@ -1793,13 +1791,22 @@ class _SignupScreenState extends State<SignupScreen> {
hasTypeCount, hasTypeCount,
), ),
if (requiresUpper) if (requiresUpper)
_cryptoCheck(tr('msg.userfront.signup.password.rule.uppercase'), hasUpper), _cryptoCheck(
tr('msg.userfront.signup.password.rule.uppercase'),
hasUpper,
),
if (requiresLower) if (requiresLower)
_cryptoCheck(tr('msg.userfront.signup.password.rule.lowercase'), hasLower), _cryptoCheck(
tr('msg.userfront.signup.password.rule.lowercase'),
hasLower,
),
if (requiresNumber) if (requiresNumber)
_cryptoCheck(tr('msg.userfront.signup.password.rule.number'), hasDigit), _cryptoCheck(tr('msg.userfront.signup.password.rule.number'), hasDigit),
if (requiresSymbol) if (requiresSymbol)
_cryptoCheck(tr('msg.userfront.signup.password.rule.symbol'), hasSpecial), _cryptoCheck(
tr('msg.userfront.signup.password.rule.symbol'),
hasSpecial,
),
]; ];
return LayoutBuilder( return LayoutBuilder(
@@ -1861,14 +1868,15 @@ class _SignupScreenState extends State<SignupScreen> {
obscureText: _isPasswordObscured, obscureText: _isPasswordObscured,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
decoration: InputDecoration( decoration: InputDecoration(
labelText: tr('ui.userfront.signup.password.label'), labelText: tr(
'ui.userfront.signup.password.label',
),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: _passwordError, errorText: _passwordError,
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
_isPasswordObscured = _isPasswordObscured = !_isPasswordObscured;
!_isPasswordObscured;
}); });
}, },
icon: Icon( icon: Icon(
@@ -1897,8 +1905,8 @@ class _SignupScreenState extends State<SignupScreen> {
obscureText: _isConfirmPasswordObscured, obscureText: _isConfirmPasswordObscured,
onChanged: (val) { onChanged: (val) {
setState(() { setState(() {
_confirmPasswordError = (val != _confirmPasswordError =
_passwordController.text) (val != _passwordController.text)
? tr('msg.userfront.signup.password.mismatch') ? tr('msg.userfront.signup.password.mismatch')
: null; : null;
}); });
@@ -2032,11 +2040,7 @@ class _SignupScreenState extends State<SignupScreen> {
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(isDesktop ? 16 : 14), padding: EdgeInsets.all(isDesktop ? 16 : 14),
child: Wrap( child: Wrap(spacing: 12, runSpacing: 10, children: checks),
spacing: 12,
runSpacing: 10,
children: checks,
),
), ),
); );
} }

View File

@@ -33,7 +33,11 @@ class MockProfileNotifier extends ProfileNotifier {
} }
@override @override
Future<void> updateProfile({String? name, String? phone, String? department}) async { Future<void> updateProfile({
String? name,
String? phone,
String? department,
}) async {
updateCalled = true; updateCalled = true;
updatedName = name; updatedName = name;
_profile = _profile!.copyWith( _profile = _profile!.copyWith(
@@ -46,75 +50,82 @@ class MockProfileNotifier extends ProfileNotifier {
} }
void main() { void main() {
testWidgets('ProfilePage explicit save button UX flow (Edit -> Cancel -> Edit -> Save)', (tester) async { testWidgets(
final recordedErrors = <FlutterErrorDetails>[]; 'ProfilePage explicit save button UX flow (Edit -> Cancel -> Edit -> Save)',
final previousOnError = FlutterError.onError; (tester) async {
FlutterError.onError = (details) { final recordedErrors = <FlutterErrorDetails>[];
final text = details.exceptionAsString(); final previousOnError = FlutterError.onError;
if (text.contains('A RenderFlex overflowed')) { FlutterError.onError = (details) {
return; final text = details.exceptionAsString();
} if (text.contains('A RenderFlex overflowed')) {
recordedErrors.add(details); return;
}; }
addTearDown(() { recordedErrors.add(details);
FlutterError.onError = previousOnError; };
}); addTearDown(() {
FlutterError.onError = previousOnError;
});
tester.view.physicalSize = const Size(1920, 1080); tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0; tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetDevicePixelRatio);
final mockNotifier = MockProfileNotifier(); final mockNotifier = MockProfileNotifier();
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [profileProvider.overrideWith(() => mockNotifier)],
profileProvider.overrideWith(() => mockNotifier), child: const MaterialApp(home: Scaffold(body: ProfilePage())),
],
child: const MaterialApp(
home: Scaffold(body: ProfilePage()),
), ),
), );
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// 1. Entering edit mode // 1. Entering edit mode
final editButton = find.byKey(const Key('profile-name-edit-button')); final editButton = find.byKey(const Key('profile-name-edit-button'));
expect(editButton, findsOneWidget); expect(editButton, findsOneWidget);
await tester.tap(editButton); await tester.tap(editButton);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final inputField = find.byKey(const Key('profile-name-input')); final inputField = find.byKey(const Key('profile-name-input'));
expect(inputField, findsOneWidget); expect(inputField, findsOneWidget);
// 2. Testing cancel flow // 2. Testing cancel flow
await tester.enterText(inputField, 'Changed Name'); await tester.enterText(inputField, 'Changed Name');
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final cancelButton = find.byKey(const Key('profile-name-cancel-button')); final cancelButton = find.byKey(const Key('profile-name-cancel-button'));
await tester.tap(cancelButton); await tester.tap(cancelButton);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// After cancellation, the field should be read-only again. // After cancellation, the field should be read-only again.
expect(find.byKey(const Key('profile-name-input')), findsNothing); expect(find.byKey(const Key('profile-name-input')), findsNothing);
// Find text could be part of ListTile // Find text could be part of ListTile
expect(find.text('Original Name'), findsWidgets); expect(find.text('Original Name'), findsWidgets);
// 3. Re-enter edit mode and explicitly save // 3. Re-enter edit mode and explicitly save
await tester.tap(find.byKey(const Key('profile-name-edit-button'))); await tester.tap(find.byKey(const Key('profile-name-edit-button')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('profile-name-input')), 'Saved Name'); await tester.enterText(
await tester.pumpAndSettle(); find.byKey(const Key('profile-name-input')),
'Saved Name',
);
await tester.pumpAndSettle();
final saveButton = find.byKey(const Key('profile-name-save-button')); final saveButton = find.byKey(const Key('profile-name-save-button'));
await tester.tap(saveButton); await tester.tap(saveButton);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 4));
await tester.pumpAndSettle();
// Verify the mock received the update FlutterError.onError = previousOnError;
expect(mockNotifier.updateCalled, isTrue);
expect(mockNotifier.updatedName, 'Saved Name'); // Verify the mock received the update
}); expect(mockNotifier.updateCalled, isTrue);
expect(mockNotifier.updatedName, 'Saved Name');
expect(recordedErrors, isEmpty);
},
);
} }