透過 Playwright 的 Mock API 機制,我們已經有了非常簡單的後端整合機制,接下來我們要繼續加上結帳的按鈕並且模擬成功跟失敗的兩種情境,讓我們在不依賴後端的狀況下完成一個非常基礎的購物車前端實現。
新增規格
在結帳的機制上,我們預期如果有商品就回傳「結帳成功」反之則會得到「結帳失敗」的訊息,因此在原本的 features/product.feature
文件中繼續增加新的描述。
1#language:zh-TW
2# ...
3 場景: 蒼時購物車有物品時,可以進行結帳並且看到成功的結果
4 當 把 "Ruby 秘笈" 加入購物車
5 並且 進行結帳
6 那麼 可以看見購物車總金額為 "$0"
7 並且 可以看見購物車商品數為 0
8 並且 可以看見結帳結果為 "結帳成功"
9
10
11 場景: 蒼時購物車沒有物品時,可以進行結帳並且看到失敗的結果
12 當 進行結帳
13 那麼 可以看見購物車總金額為 "$0"
14 並且 可以看見購物車商品數為 0
15 並且 可以看見結帳結果為 "結帳失敗"
這兩條描述呈現了成功跟失敗的情況外,也要求送出後會將購物車清空的行為,因此我們需要再實作時也將這點考慮進去。
因為增加了兩個新的步驟,繼續擴充 features/step_definitions/common.ts
來讓 Cucumber 知道該如何處理這兩種情況。
1// ...
2When('進行結帳', async function(this: World) {
3 await this.page.getByRole('button', { name: '結帳' }).click()
4})
5
6// ...
7Then('可以看見結帳結果為 {string}', async function(this: World, result: string) {
8 await expect(this.page.getByText(result)).toBeVisible()
9})
跟前面的行為相比,因為相對的單純,我們可以直接尋找「結帳」的按鈕點選,以及確認頁面上是否存在「結帳成功」或者「結帳失敗」來做驗證。
如果頁面非常複雜,明確的指定或者縮小範圍還是會讓測試跑得更快一些,尤其時這些測試都有一個超時的設定,單一步驟處理過久不是一件好事。
Mock API
跟商品資料不同的地方在於,結帳行為理論上要由後端處理,並且不應該有預期外的行為產生。因此我們不會利用假設(Given)的方式定義,因此直接在 feature/support/world.ts
中進行擴充,讓這個路徑可以被呼叫。
1// ...
2
3Before(async function() {
4 await this.page.route('/api/checkout', async route => {
5 const cartItems = route.request().postDataJSON()
6 const json = { text: '結帳成功' }
7 const isValid = Object.values(cartItems).length > 0
8
9 if(!isValid) {
10 json.text = '結帳失敗'
11 }
12
13 await route.fulfill({
14 status: isValid ? 200 : 422,
15 json
16 })
17 })
18})
當我們有實際的後端可以使用時,可以選擇把這段邏輯拆除。在這裡我們從請求中取得 JSON 物件,並且根據傳入的購物車數量決定要回傳成功還是失敗的訊息。
在 Playwright API 中,可以呼叫
route.fetch()
來實際發出請求,並且利用route.fulfill()
來改寫回傳內容,除了直接模擬行為外,也可以善用改寫機制來輔助測試。
實作結帳
最後我們將結帳的實作加入到 src/App.vue
裡面來完成這個功能,先在原本的購物車區段加入新的按鈕與結果的區塊。
1<template>
2 <div class="cart">
3 <div v-if="hadCheckout">{{ checkoutResult }}</div>
4 <div data-testid="cart-amount">一共 {{ totalItems }} 項商品</div>
5 <div data-testid="cart-subtotal">總金額為 ${{ subtotal }}</div>
6 <button @click="onCheckout">結帳</button>
7 </div>
8 <!--more-->
9</template>
這次增加了 onCheckout
方法來送出購物車,以及 hadCheckout
和 checkoutResult
來對應結帳後的結果,繼續修改 TypeScript 的部分加入對應的實作。
1<script setup lang="ts">
2// ...
3
4const checkoutResult = ref(null)
5const onCheckout = () => fetch('/api/checkout', {
6 method: "POST",
7 headers: {
8 "Content-Type": "application/json",
9 },
10 body: JSON.stringify(cart)
11})
12 .then(res => {
13 if(res.ok) {
14 for(let id in cart) {
15 delete cart[id]
16 }
17 }
18
19 return res
20 })
21 .then(res => res.json())
22 .then(data => checkoutResult.value = data.text)
23const hadCheckout = computed(() => checkoutResult.value !== null)
24</script>
這裡的結果會需要觸發 Vue 重新繪製畫面,因此我們需要用 ref(null)
來初始化,並且用 computed()
來檢查是否為 null
如果已經有 API 的結果回傳,我們就可以將內容顯示出來。
在 onCheckout
方法的實作上,我們直接使用 fetch()
來對 /api/checkout
端點進行呼叫,並且在回傳結果為成功(Response.ok
表示狀態碼在 200
~ 299
之間)的時候對購物車進行清空的處理。
因為 reactive({})
只對修改物件生效,因此我們無法用 cart = {}
這類方式修改,只能呼叫迴圈將所有被紀錄的商品找出,用 delete cart[id]
的方式清除掉記憶體的資訊。
最後,回傳是以文字訊息的方式呈現,因此直接將結果放到 checkoutResult
中顯示出來即可。
到此為止,一個非常簡單的購物功能就完成,我們需要對現有的實作進行一些調整來支援跟後端的整合處理。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 同時完成測試與文件 - Cucumber 的文件測試法
- 基本語法:功能描述 - Cucumber 的文件測試法
- 基本語法:驗證行為 - Cucumber 的文件測試法
- 基本語法:步驟定義 - Cucumber 的文件測試法
- 基本語法:輔助設定 - Cucumber 的文件測試法
- 前端環境:Vite 與 Cucumber - Cucumber 的文件測試法
- 商品列表與加入購物車 - Cucumber 的文件測試法
- 重構與移出購物車 - Cucumber 的文件測試法
- 商品資料與總價 - Cucumber 的文件測試法
- 結帳與結果 - Cucumber 的文件測試法
- 整理前端實作 - Cucumber 的文件測試法
- 初始化後端專案 - Cucumber 的文件測試法
- 商品資料 API - Cucumber 的文件測試法
- 更新購物車 API - Cucumber 的文件測試法
- 加入資料模型 - Cucumber 的文件測試法
- 持久化保存 - Cucumber 的文件測試法
- 結帳處理 - Cucumber 的文件測試法
- 在 Rails 的前後端分離 - Cucumber 的文件測試法
- 匯入前端實作 - Cucumber 的文件測試法
- 重現後端實作 - Cucumber 的文件測試法
- 累積價值 - Cucumber 的文件測試法