บทที่ 13 · Part 3 — UI and API Capabilities

Network Control and Dependencies

รอ response อย่างมีเหตุผล mock เฉพาะ boundary และใช้ route/HAR จำลอง failure ที่สร้างจริงได้ยาก

Network Control and Dependencies

notification service ตอบ 503 เพียงบางครั้งใน test environment ทีมจึงไม่เคยเห็น UI state “จองสำเร็จแต่ส่ง reminder ไม่สำเร็จ” แบบ deterministic การควบคุม network ช่วยสร้าง rare state ได้ แต่ mock มากเกินไปจะทำให้ test ผ่านแม้ contract จริงเปลี่ยน

จบบทนี้คุณจะ

ใช้ waitForResponse, route, fulfill, fetch, abort และ HAR replay เลือก boundary ที่ควร mock ลง route ก่อน request และรักษา integration coverage เพื่อจับ contract drift

รอ Response เมื่อ Response เป็น Claim

ถ้าต้องตรวจ request/response ที่ action ส่ง ให้ตั้ง promise ก่อนคลิก:

const responsePromise = page.waitForResponse((response) =>
  response.url().endsWith('/api/reservations') &&
  response.request().method() === 'POST',
)

await page.getByRole('button', { name: 'Reserve seat' }).click()
const response = await responsePromise

expect(response.status()).toBe(201)
await expect(page.getByRole('heading', { name: 'Reservation confirmed' })).toBeVisible()

ถ้าตรวจ visible result อย่างเดียวก็ไม่ต้องรอ network การผูก endpoint เพิ่ม coupling โดยไม่เพิ่ม evidence ใช้ network wait เมื่อ status/payload/request shape คือ behavior ที่กำลังทดสอบหรือจำเป็นต่อ diagnosis

Mock Dependency Failure ด้วย Route

ลง route ก่อน navigation/action ที่ส่ง request:

await page.route('**/api/reminders', async (route) => {
  await route.fulfill({
    status: 503,
    contentType: 'application/json',
    body: JSON.stringify({
      error: { code: 'REMINDER_UNAVAILABLE' },
    }),
  })
})

await page.goto(`/workshops/${workshop.slug}`)
await page.getByRole('button', { name: 'Reserve seat' }).click()

await expect(page.getByRole('heading', { name: 'Reservation confirmed' })).toBeVisible()
await expect(page.getByRole('status')).toContainText(
  'Reserved. Reminder will be retried later.',
)

Test นี้พิสูจน์ frontend behavior ต่อ known 503 contract ไม่ได้พิสูจน์ reminder service จริง จึงต้องมี API/integration suite ที่เรียก dependency test environment หรือ consumer contract แยก

Inspect Request ก่อน Fulfill

await page.route('**/api/reservations', async (route) => {
  const payload = route.request().postDataJSON() as { workshopId: string }
  expect(payload.workshopId).toBe(workshop.id)

  await route.fulfill({
    status: 201,
    json: {
      data: { id: 'res-test-001', status: 'CONFIRMED' },
    },
  })
})

ใช้ fixed mock id ได้เพราะอยู่ใน isolated browser test และไม่แตะ server state แต่ถ้า parallel pages แชร์ context route ต้องแน่ใจว่า match ไม่กว้างจนจับ request ของ test อื่น Built-in test page มี context แยกต่อ test อยู่แล้ว

Modify Real Response ด้วย route.fetch()

await page.route('**/api/workshops/*', async (route) => {
  const response = await route.fetch()
  const body = await response.json()
  body.data.seatsRemaining = 0

  await route.fulfill({ response, json: body })
})

Pattern นี้รักษา headers/status ส่วนใหญ่และแก้ field ที่ต้องสร้าง state แต่ยังผูกกับ upstream availability ถ้า response schema เปลี่ยน test อาจ fail ซึ่งบางครั้งเป็นประโยชน์ ระบุชัดว่าเป็น modified integration หรือ full stub

Continue และ Abort

await page.route('**/analytics/**', (route) => route.abort())
await page.route('**/api/**', (route) => route.continue())

Abort analytics ช่วยพิสูจน์ app ยังทำ core journey ได้เมื่อ non-critical script/network หาย แต่ห้าม block dependency จนซ่อน behavior ที่ต้องรองรับจริง Route pattern กว้างต้อง review เพราะลำดับ handlers และ popup requests อาจต่าง ถ้าต้องครอบทุก page/popup ใช้ browserContext.route()

Service Worker Caveat

Request ที่ Service Worker intercept อาจไม่ปรากฏให้ native routing เห็น หาก suite ต้องควบคุม network ด้วย route ให้ตั้ง serviceWorkers: 'block' ใน project นั้นตาม network documentation แต่ test PWA/offline behavior ต้องเปิด Service Worker และใช้ strategy อื่น แยก projects เพราะ 2 claims ขัดกัน

HAR Replay

await page.routeFromHAR('tests/har/workshop-catalog.har', {
  url: '**/api/catalog/**',
  notFound: 'abort',
})

await page.goto('/workshops')

HAR เหมาะกับ read-heavy flow ที่มีหลาย responses และต้อง replay deterministic Matching ใช้ URL + method และ POST payload เมื่อเกี่ยวข้อง notFound: 'abort' ทำให้ unrecorded call fail ชัด ส่วน fallback ส่งต่อ network เลือกตาม test intent

Record/update HAR เป็น controlled maintenance task ไม่เปิด update บน CI โดยอัตโนมัติ เพราะ upstream drift อาจถูก approve เป็น baseline ใหม่โดยไม่ review HAR มี headers, cookies และ bodies ต้อง inspect/redact ก่อน commit หากมีข้อมูลอ่อนไหวให้สร้าง synthetic HAR หรือไม่ commit เลย

Mocking Decision Table

NeedToolEvidence ที่ได้Evidence ที่ไม่ได้
รอ create responsewaitForResponserequest จริงเกิดและ status ตาม environmentdeterministic rare failure
UI state จาก known errorroute.fulfillfrontend mapping/renderingupstream contract ยังเหมือนจริง
แก้ field จาก real responseroute.fetch + fulfillintegration ส่วนใหญ่ + controlled variationfull real behavior ของ field ที่แก้
หลาย read endpoints คงที่HAR replaydeterministic recorded contractcurrent upstream drift
dependency unreachableroute.abortapp network-failure behaviorserver-specific error body

Contract Drift Strategy

แยก suites อย่างน้อย:

  • deterministic UI tests ที่ mock rare/error states
  • API/contract tests ที่ pin schemas/error codes
  • integration smoke ที่ไม่ mock critical dependencies

เมื่อ mock schema เปลี่ยน ให้ owner ของ consuming UI และ producing API review พร้อมกัน อย่าปล่อย mocks เป็น JSON ที่ไม่มี type/schema validation จนกลายเป็น fiction

รายการตรวจสอบ

  • Listener/route ถูกลงก่อน request
  • Mock อยู่ที่ boundary ที่ test ไม่ได้กำลังพิสูจน์
  • Route scope แคบและ request assertions ตรง intent
  • Service Worker policy ตรง claim ของ project
  • HAR เป็น synthetic/redacted และ update ผ่าน review
  • มี unmocked contract/integration coverage ของ dependency สำคัญ

สรุปบทนี้

Network control ทำ rare states ให้ reproducible แต่ทุก mock ตัด evidence บางส่วน Decision table และ unmocked suite ช่วยให้ deterministic feedback ไม่แลกกับการมองไม่เห็น contract drift

อ่านเพิ่มเติม