延續現有的「加入購物車」功能,我們要繼續加入「移出購物車」的機制,因為原本的設計只是單純的滿足計數,這次還需要實現實際存在的商品列表來反應現實狀況。
重構
目前我們是將商品寫死,並且直接統計點選「加入購物車」的次數,在不破壞原有測試的前提下,我們要改為動態的顯示商品列表,並且以商品的編號(ID)來統計數量。
1<template>
2 <div data-testid="cart">一共 {{ totalItems }} 項商品</div>
3 <div>
4 <div v-for="product in products" :key="product.id" :data-testid="'product-' + product.id">
5 {{ product.name }}
6 <button @click="onAddToCart(product.id)">加入購物車</button>
7 </div>
8 </div>
9</template>
10<!-- 略 -->
首先,我們把原本的 {{ cart.count }}
替換成 {{ totalItems }}
這個使用 computed()
產生的方法,然後將原本的商品列表用 v-for
依序載入進來,並且用 :data-testid="'product-' + product.id"
來確保測試用標記有被加上去。
繼續修改 TypeScript 的部分,將 totalItems
的實現放到裡面,以及修改 onAddToCart
為可以接受一個商品編號的版本。
1<!-- 略 -->
2<script setup lang="ts">
3import { reactive, computed } from 'vue'
4
5const products = {
6 1: { id: 1, name: 'Ruby 秘笈' },
7 2: { id: 2, name: 'RSpec 秘笈' }
8}
9
10const cart = reactive({})
11const onAddToCart = (id) => {
12 cart[id] = cart[id] || 0
13 cart[id] += 1
14}
15
16const totalItems = computed(() => Object.values(cart).reduce((prev, curr) => prev + curr, 0))
17</script>
我們加入了 products
來提供商品資訊,並且把原本的 cart
從 reactive({ count: 0 })
修改為 reactive({})
來保存一個 Key-Value 對應的購物車品項資訊。
針對 onAddToCart()
的修改,則是從原本的單純增加數量,改為 cart[id] += 1
來對指定的品項進行數量上的調整。
最後,我們用 Object.values(cart)
取出所有品項的數量,利用 Array.reduce()
來加總,這邊要特別注意的是我們必須提供 Array.reduce(() => ... ,0)
第二個參數(初始值)來確保購物車為空時,能有一個預設值。
重新運行 yarn run cucumber-js
命令,會發現我們順利的通過測試,也完成了一次重構。
移出購物車
現在我們已經有了 onAddToCart()
方法可以以商品編號進行對應,那麼要實現 onRemoveFromCart()
方法來將商品從購物車移除就容易許多,在這之前先補上新的 Cucumber 功能描述來驗證這件事情。
1#language:zh-TW
2功能: 商品列表
3 # ...
4 場景: 蒼時可以把已經在購物車的 Ruby 秘笈移出,並看到商品數為 1
5 當 把 "Ruby 秘笈" 加入購物車
6 並且 把 "RSpec 秘笈" 加入購物車
7 並且 把 "Ruby 秘笈" 移出購物車
8 那麼 可以看見購物車商品數為 1
因為多了一個「移出購物車」的步驟,我們還需要將新的步驟定義加入到 features/step_definitions/common.ts
裡面。
1// ...
2When('把 {string} 移出購物車', async function(this: World, productName: string) {
3 const product = await this.page.getByTestId(/product-/).filter({ hasText: productName })
4 await product.getByRole('button', { name: '移出購物車' }).click()
5})
基本上跟加入購物車的實作是一樣的,不過按鈕被替換成移出購物車來進行點選。
另一種邏輯是定義一個「對商品 “Ruby 秘笈” 進行 “移出購物車” 的操作」來反應這件事情,就可以動態支援不同的按鈕。
回到 Vue 的部分,我們在加入購物車後面放入移出購物車的按鈕。
1<template>
2 <!-- 略 -->
3 {{ product.name }}
4 <button @click="onAddToCart(product.id)">加入購物車</button>
5 <button @click="onRemoveFromCart(product.id)" v-if="isInCart(product.id)">移出購物車</button>
6 </div>
7 <!-- 略 -->
8</template>
9
10<!-- 略 -->
為了在沒有任何品項時不會被點擊,我們也另外定義了一個 isInCart()
方法用來檢查目前的狀態,假設品項不存在的話就不要渲染出來(如果有效能考量,也可以考慮 v-show
的方式)
1<!-- 略 -->
2<script setup lang="ts">
3import { reactive, computed } from 'vue'
4
5// ...
6const onRemoveFromCart = (id) => {
7 if(!isInCart(id)) {
8 return
9 }
10
11 cart[id] -= 1
12}
13const isInCart = (id) => cart[id] && cart[id] > 0
14// ...
15</script>
最後在 TypeScript 端補上對應的實作,將 isInCart()
和 onRemoveFromCart()
實作進去,再運行 yarn run cucumber-js
就可以順利通過新加入的測試,也完善了新功能。
目前的程式碼看起來「不太乾淨」是在預期內的,所以還不需要急著將程式碼進行重構,可以等到下一次修改再少量的調整,因為我們已經有了一定程度的測試保護,在重構上的影響不會對現有功能造成太大的衝擊。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 同時完成測試與文件 - 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 的文件測試法