# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: 4373/dev/rounds/b1ea8ff3/vendor-save.spec.ts >> [dev] / [TA] role / 사용자 지정 가맹점 저장·원복 >> 08 선택·거래가능·정산 시간 저장·재진입
- Location: qa/issues/4373/dev/rounds/b1ea8ff3/vendor-save.spec.ts:9:78

# Error details

```
Error: expect(locator).toHaveValue(expected) failed

Locator:  getByRole('dialog').last().getByRole('textbox', { name: '정산 시작 시간', exact: true })
Expected: "10:25"
Received: ""
Timeout:  7000ms

Call log:
  - Expect "toHaveValue" with timeout 7000ms
  - waiting for getByRole('dialog').last().getByRole('textbox', { name: '정산 시작 시간', exact: true })
    18 × locator resolved to <input value="" id="_r_an_" type="text" inputmode="text" autocomplete="off" placeholder="hh:mm" aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputSizeSmall MuiInputBase-inputAdornedEnd css-b52kj1"/>
       - unexpected value ""

```

```yaml
- textbox "정산 시작 시간":
  - /placeholder: hh:mm
```

# Test source

```ts
  1  | import {test,expect,type Page,type Locator} from '@playwright/test';
  2  | import fs from 'node:fs';
  3  | import {getRuntimeQaEnv} from '../../../../../../src/qa/env';
  4  | import {createStorageStateForRole} from '../../../../../../src/qa/storageState';
  5  |
  6  | test.use({channel:'chrome'});
  7  | for (const role of (process.env.QA4373_SAVE_ONLY_ROLE?[process.env.QA4373_SAVE_ONLY_ROLE]:['ADMIN','TA'])) test.describe.serial(`[dev] / [${role}] role / 사용자 지정 가맹점 저장·원복`,()=>{
  8  |  test.beforeAll(async()=>{test.skip(process.env.QA_4373_ALLOW_VENDOR_MUTATION!=='1','BLOCKED: 테스트 가맹점 변경 guard 필요');await createStorageStateForRole({application:'admin',environment:'dev',role:role==='ADMIN'?'QA4373_ADMIN':role==='CO'?(process.env.QA4373_SAVE_CO_ENV_ROLE??role):role});});
  9  |  for(const scenario of ['05 마지막 글자 입력 직후 저장·재진입','08 선택·거래가능·정산 시간 저장·재진입']) test(scenario,async({browser},info)=>{
  10 |   const targetName=process.env.QA4373_SAVE_TARGET??'박재민_가맹_한결데';test.setTimeout(120000);fs.mkdirSync('qa-results/4373-owner-b1ea8ff3/save-backups',{recursive:true});
  11 |   const env=getRuntimeQaEnv('dev','admin',role==='ADMIN'?'QA4373_ADMIN':role==='CO'?(process.env.QA4373_SAVE_CO_ENV_ROLE??role):role),ctx=await browser.newContext({storageState:env.storageState});
  12 |   const page=await ctx.newPage();page.setDefaultTimeout(10000);
  13 |   let form!:Locator;let id='',dirty=false,restored=false;
  14 |   let original:{name:string;note:string;vendorType:string;start:string;end:string;settlementStart:string;settlementEnd:string}|undefined;const timeObservations:any[]=[];page.on('response',async r=>{if(id&&r.request().method()==='GET'&&new URL(r.url()).pathname.endsWith(`/v2/vendors/${id}`)){try{const p=(await r.json()).payload;timeObservations.push({source:'GET',settlementStart:p?.settlementInfo?.settlementStartTime??p?.settlementStartTime,settlementEnd:p?.settlementInfo?.settlementEndTime??p?.settlementEndTime,start:p?.paymentPolicy?.paymentStartTime,end:p?.paymentPolicy?.paymentEndTime});}catch{}}});
  15 |   const field=(name:string)=>form.locator(`[name="${name}"]`).filter({visible:true}).first();
  16 |   const time=(name:string)=>form.getByRole('textbox',{name,exact:true});
  17 |   async function open(){
  18 |    const popup=page.locator('.swal2-popup:visible');
  19 |    if(await popup.count()){
  20 |     const cancel=popup.getByRole('button',{name:'취소',exact:true});
  21 |     if(await cancel.count())await cancel.click();
  22 |     const ok=page.locator('.swal2-popup:visible').getByRole('button',{name:/^(OK|확인)$/});
  23 |     if(await ok.count())await ok.click();
  24 |    }
  25 |    const current=page.getByRole('dialog');
  26 |    if(await current.count())await current.last().getByRole('button',{name:'취소',exact:true}).click();
  27 |    const row=page.locator(`[data-route-keep-alive-active=true] .MuiDataGrid-row[data-id="${id}"]`);
  28 |    await expect(row).toBeVisible({timeout:20000});await row.locator('svg.lucide-pencil').click();
  29 |    form=page.getByRole('dialog').last();await expect(field('name')).toBeVisible();
  30 |    await expect(field('name')).not.toHaveValue('');
  31 |   }
  32 |   async function chooseType(value:string){
  33 |    const labels:{[key:string]:string}={TERMINAL:'단말기',MANUAL:'수기',MANUAL_AND_TERMINAL:'수기+단말기',UNSPECIFIED:'설정안함'};
  34 |    await form.locator('[name=vendorType]').locator('..').getByRole('combobox').click();
  35 |    await page.getByRole('listbox').getByText(labels[value],{exact:true}).click();
  36 |   }
  37 |   async function save(){
  38 |    const response=page.waitForResponse(r=>r.request().method()==='PATCH'&&new URL(r.url()).pathname.endsWith(`/v2/vendors/${id}`),{timeout:15000});
  39 |    await form.getByRole('button',{name:'수정',exact:true}).click();
  40 |    await page.getByPlaceholder('변경사유',{exact:true}).fill('QA #4373 사용자 지정 가맹점 저장 검증 및 원래 값 원복');
  41 |    await page.locator('.swal2-popup:visible').getByRole('button',{name:'확인',exact:true}).click();
  42 |    const result=await response;const submitted=result.request().postDataJSON();timeObservations.push({source:'PATCH',settlementStart:submitted?.settlementInfo?.settlementStartTime??submitted?.settlementStartTime,settlementEnd:submitted?.settlementInfo?.settlementEndTime??submitted?.settlementEndTime,start:submitted?.paymentPolicy?.paymentStartTime,end:submitted?.paymentPolicy?.paymentEndTime,status:result.status()});expect(result.status()).toBe(200);await expect(form).not.toBeVisible({timeout:15000});
  43 |   }
  44 |   try{
  45 |    await page.goto(`${env.baseUrl}/business/vendor-manage?tenantId=redacted}`);
  46 |    const active=page.locator('[data-route-keep-alive-active=true]');await expect(active.locator('.MuiDataGrid-row').first()).toBeVisible({timeout:20000});
  47 |    await active.getByText('상세검색',{exact:true}).click();await active.getByText('직접 입력',{exact:true}).click();
  48 |    await active.getByRole('textbox',{name:'가맹점명',exact:true}).fill(process.env.QA4373_SAVE_TARGET??'한결데');
  49 |    await active.getByRole('button',{name:'검색',exact:true}).click();
  50 |    const target=active.locator('.MuiDataGrid-row').filter({hasText:targetName});await expect(target).toHaveCount(1,{timeout:12000}).catch(()=>test.skip(true,'BLOCKED: 해당 역할에서 사용자 지정 QA 가맹점 조회되지 않음'));await expect(target).toHaveCount(1);
  51 |    id=(await target.getAttribute('data-id'))!;expect(id).toMatch(/^[\w-]+$/);
  52 |    await open();await expect(field('name')).toHaveValue(targetName);
  53 |    original={name:await field('name').inputValue(),note:await field('note').inputValue(),vendorType:await form.locator('[name=vendorType]').inputValue(),start:await time('거래가능 시작 시간').inputValue(),end:await time('거래가능 종료 시간').inputValue(),settlementStart:await time('정산 시작 시간').inputValue(),settlementEnd:await time('정산 종료 시간').inputValue()};
  54 |    fs.mkdirSync('qa-results/4373-owner-b1ea8ff3/save-backups',{recursive:true});
  55 |    fs.writeFileSync(`qa-results/4373-owner-b1ea8ff3/save-backups/${role}-${info.title.slice(0,2)}-before.json`,JSON.stringify({id,...original},null,2));
  56 |    dirty=true;
  57 |    if(scenario.startsWith('05')){
  58 |     const name=original.name+'끝',note='QA4373 실제 저장 마지막글자끝';
  59 |     await field('name').fill(original.name);await field('name').pressSequentially('끝',{delay:1});
  60 |     await field('note').fill(note.slice(0,-1));await field('note').pressSequentially('끝',{delay:1});
  61 |     await save();await page.reload();await open();
  62 |     await expect(field('name')).toHaveValue(name);await expect(field('note')).toHaveValue(note);
  63 |     await info.attach('05-saved-last-character.png',{body:await form.screenshot(),contentType:'image/png'});
  64 |    }else{
  65 |     const selected=original.vendorType==='UNSPECIFIED'?'MANUAL':'UNSPECIFIED';
  66 |     await chooseType(selected);await time('거래가능 시작 시간').fill('09:15');await time('거래가능 종료 시간').fill('18:45');
  67 |     await time('거래가능 종료 시간').press('Tab');await time('정산 시작 시간').fill('10:25');await time('정산 종료 시간').fill('19:45');await time('정산 종료 시간').press('Tab');await save();await page.reload();await open();
  68 |     await info.attach('08-save-reentry.json',{body:JSON.stringify({role,observations:timeObservations,afterReloadUi:{settlementStart:await time('정산 시작 시간').inputValue(),settlementEnd:await time('정산 종료 시간').inputValue(),start:await time('거래가능 시작 시간').inputValue(),end:await time('거래가능 종료 시간').inputValue()}}),contentType:'application/json'});await info.attach('08-reentry.png',{body:await form.screenshot(),contentType:'image/png'});await expect(form.locator('[name=vendorType]')).toHaveValue(selected);
> 69 |     await expect(time('정산 시작 시간')).toHaveValue('10:25');await expect(time('정산 종료 시간')).toHaveValue('19:45');await expect(time('거래가능 시작 시간')).toHaveValue('09:15');await expect(time('거래가능 종료 시간')).toHaveValue('18:45');
     |                                    ^ Error: expect(locator).toHaveValue(expected) failed
  70 |     await info.attach('08-saved-selection-times.png',{body:await form.screenshot(),contentType:'image/png'});
  71 |    }
  72 |   }finally{
  73 |    try{
  74 |     if(dirty&&original){
  75 |      await open();await field('name').fill(original.name);await field('note').fill(original.note);
  76 |      await chooseType(original.vendorType);await time('거래가능 시작 시간').fill(original.start);await time('거래가능 종료 시간').fill(original.end);await time('정산 시작 시간').fill(original.settlementStart);await time('정산 종료 시간').fill(original.settlementEnd);await time('거래가능 종료 시간').press('Tab');
  77 |      await save();await page.reload();await open();
  78 |      await expect(field('name')).toHaveValue(original.name);await expect(field('note')).toHaveValue(original.note);await expect(form.locator('[name=vendorType]')).toHaveValue(original.vendorType);await expect(time('거래가능 시작 시간')).toHaveValue(original.start);await expect(time('거래가능 종료 시간')).toHaveValue(original.end);
  79 |      await expect(time('정산 시작 시간')).toHaveValue(original.settlementStart);await expect(time('정산 종료 시간')).toHaveValue(original.settlementEnd);restored=true;await info.attach('restoration.json',{body:JSON.stringify({restored:true,nameRestored:true,noteRestored:true,selectionRestored:true,timesRestored:true}),contentType:'application/json'});
  80 |     }
  81 |    }finally{
  82 |     fs.writeFileSync(`qa-results/4373-owner-b1ea8ff3/save-backups/${role}-${info.title.slice(0,2)}-restoration.json`,JSON.stringify({targetId:id,attempted:dirty,restored}));
  83 |     await ctx.close();
  84 |    }
  85 |   }
  86 |  });
  87 | });
  88 |
```