0%

CSS Scroll Snap 是一種用於控制滾動行為的 CSS 模組,允許開發者在使用者停止滾動時,自動將視圖對齊到特定的元素。這種功能常用於創造更流暢的用戶體驗,例如分頁文章、圖片輪播等。

主要由兩個屬性來控制:

scroll-snap-type:設置在滾動容器(父元素)上,用於定義滾動軸向和滾動對齊的行為。

格式為:scroll-snap-type: [軸向] [行為]
軸向可以是 x(水平滾動)、y(垂直滾動)或 both(同時支持水平與垂直)。
行為可以是 mandatory 或 proximity:
mandatory:滾動必須對齊到指定位置,確保每次停下來都會對齊。
proximity:當滾動接近某個指定的位置時,會進行對齊。
例如:

1
2
3
.scroll-container {
scroll-snap-type: y mandatory; /* 在垂直方向強制對齊 */
}

scroll-snap-align:設置在滾動內容項目(子元素)上,定義這些元素在滾動容器中的對齊方式。

可選的值有 start、center、end,決定項目在滾動結束時如何與容器對齊。
例如:

1
2
3
.scroll-item {
scroll-snap-align: start; /* 將項目對齊到滾動容器的頂端 */
}
屬性 描述
scroll-snap-type 定義滾動容器的捕捉行為,包括方向(x, y, both)和強制程度(mandatory, proximity)。
scroll-padding 設置容器內邊距,影響捕捉點的位置。
scroll-snap-align 定義子元素在容器中的對齊方式(start, center, end)。
scroll-snap-stop 控制是否在特定元素上停留,默認情況下僅在停止滾動時觸發。

使用時注意事項

  1. 跨瀏覽器不一致性
    雖然大多數現代瀏覽器都支持 Scroll Snap,但在行為上可能會有一些細微的差異。請務必在各種瀏覽器上測試你的實現,確保用戶能夠獲得一致的體驗。像是使用 cna I us 確認支援。
    對於某些舊版本仍然需要前綴。使用 WebKit 前綴來支持較舊的 Safari 和 Chrome。
1
2
3
4
.scroll-container {
-webkit-scroll-snap-type: y mandatory; /* 用於舊版 Safari 和 Chrome */
scroll-snap-type: y mandatory; /* 標準屬性 */
}

使用 @supports 進行功能檢測:你可以使用 @supports 檢測瀏覽器是否支持 Scroll Snap,並提供相應的回退方案。例如,如果某些瀏覽器不支持,則可以使用 JavaScript 實現類似的行為。

1
2
3
4
5
6
7
8
9
10
11
12

@supports (scroll-snap-type: y mandatory) {
.scroll-container {
scroll-snap-type: y mandatory;
}
}

@supports not (scroll-snap-type: y mandatory) {
.scroll-container {
/* 恢復樣式,或提供 JavaScript 實現滾動行為 */
}
}
  1. 預期外的滾動行為
    如果發現未對齊正確,或者容器的尺寸設置不當,可能會出現預期外的滾動行為。
    解決方案:
    設置固定尺寸:確保滾動容器和子元素的尺寸設置正確。對於橫向滾動,應確保每個項目的寬度是明確的,對於縱向滾動,應設置每個項目的高度。例如:
1
2
3
4
5
6
7
8
9
10
複製程式碼
.scroll-container {
overflow-x: scroll; /* 必須設置 overflow 屬性 */
scroll-snap-type: x mandatory; /* 水平捕捉點 */
}

.scroll-item {
width: 100%; /* 保證每個項目都佔據滾動區域 */
scroll-snap-align: start; /* scroll-snap-align 決定了子元素在滾動容器中如何對齊 */
}
  1. 性能影響

當頁面內容繁多,或包含大量動畫和捕捉點時,可能會導致性能問題。這會導致滾動卡頓,特別是在移動設備上。

解決方案:
避免過度使用捕捉點:不要對每一個小元素都設置 scroll-snap-align,僅在核心內容(如頁面段落、關鍵節點)設置捕捉點。例如,圖片輪播或長段落之間可以有捕捉點,但不需要對每個小項目設置。

除此之外,可以嘗試 lazy load:對於圖片或大型內容,使用懶加載技術(如 Intersection Observer API)可以避免一次性渲染過多內容,從而減輕滾動過程中的性能負擔。

  • 使用 Intersection Observer 實現懶加載: 用來觀察(observe)當指定元素接觸到父層以上或者是視窗的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const images = document.querySelectorAll('.lazy-load');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 替換為真正的圖片
observer.unobserve(img); // 停止觀察
}
});
});

images.forEach(image => {
observer.observe(image); // 觀察每個懶加載圖片
});

  • 減少不必要的動畫:過多的動畫和滾動事件監聽器會降低滾動性能,特別是如果動畫效果頻繁更新。例如,可以使用 requestAnimationFrame 來控制滾動動畫的頻率,避免過多的重排和重繪。或是嘗試,節流滾動事件:
1
2
3
4
5
6
7
8
9
let isScrolling;
window.addEventListener('scroll', () => {
window.clearTimeout(isScrolling);
isScrolling = setTimeout(() => {
// 在滾動結束後觸發的操作
console.log('滾動結束');
}, 66); // 節流時間間隔
});

參考文章:
超好用的 Web API - Intersection Observer
No JS required — you can do this with CSS!

image

在 Quasar 的日期選擇器,要使用 QInput + QDate 方式,可以參考文件 Date Picker

但在此次功能需求,是讓使用者只能選擇年/月。所以首先依循查詢結果,找到 monthpicker ,在其中 codepen - QDate: DATE YEAR/MONTH PICKER,透過設置 setView 屬性,使得選擇器界面可以顯示年、月以及日的選擇視圖。

自制 monthlyPicker

因此參考上方的方式,建立一個 monthlyPicker 元件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div class="q-mr-md">
<q-input
dense
v-model="monthValue"
id="date-input"
class="single-month-picker"
readonly
>
<template v-slot:append>
<q-icon
name="fa-regular fa-calendar"
class="cursor-pointer"
size="1.1rem"
>
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<div class="q-date__header flex column items-start">
<span @click="handleToMonthView" class="cursor-pointer"
>{{ "Year - Month" }}</span
>
<div class="text-h5 cursor-pointer" @click="handleToYearView">
{{ monthValue }}
</div>
</div>
<q-date
class="date-disable-btn"
dense
ref="dateRef"
v-model="monthValue"
default-view="Years"
emit-immediately
@update:model-value="onUpdate"
mask="YYYY-MM"
minimal
years-in-month-view
:navigation-min-year-month="minYearMonth"
:navigation-max-year-month="maxYearMonth"
>
<div class="row items-center justify-end date-action-btn">
<q-btn
dense
flat
v-close-popup
class="btn--no-hover"
label="Close"
color="primary"
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script setup lang="ts">
import { date, QDate } from 'quasar'

const emit = defineEmits(['selectedDate'])
const props = defineProps({
month: {
type: String,
required: true
}
})
const monthValue = ref(props.month)
const dateRef = ref<InstanceType<typeof QDate> | null>(null)
const currentView = ref<'Years' | 'Months' | 'Days'>('Years')

// computed
const minYearMonth = computed(() => {
const oneYearAgo = date.subtractFromDate(new Date(), { month: 13 })
return date.formatDate(oneYearAgo, 'YYYY/MM')
})

const maxYearMonth = computed(() => {
const oneMonthAgo = date.subtractFromDate(new Date(), { month: 1 })
return date.formatDate(oneMonthAgo, 'YYYY/MM')
})

const onUpdate = () => {
emit('selectedDate', date.formatDate(new Date(monthValue.value), 'YYYY-MM'))
setCalendarView('Months')
}

const handleToMonthView = () => {
setCalendarView('Months')
}
const handleToYearView = () => {
setCalendarView('Years')
}

const setCalendarView = (view: 'Years' | 'Months') => {
currentView.value = view
dateRef.value?.setView(view)
}
</script>

簡要說明

  • QDate 組件:設置 default-view 為 Years,讓選擇器初始顯示年份視圖。mask 設置為 YYYY-MM,確保輸出格式為年-月。
  • 計算屬性:透過 minYearMonth 和 maxYearMonth 屬性,限制使用者只能選擇過去一年內的日期範圍。
  • 視圖切換:通過 handleToMonthView 和 handleToYearView 函數來實現視圖切換,並且用 setCalendarView 函數來控制視圖變更。

外層使用該元件方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<monthly-picker :month="selectedMonth" @selectedDate="handleDateSelected" />
<p>你選擇的月份是:{{ selectedMonth }}</p>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import MonthlyPicker from "./components/MonthlyPicker.vue";

const selectedMonth = ref("2024-08");

const handleDateSelected = (date: string) => {
selectedMonth.value = date;
};
</script>

在前端開發中,有時需要從後端提供數據並生成文件提供用戶下載。以下將以下載 Excel 為例,介绍如何使用 axios 從後端取得文件,並在前端處理文件的下載過程。我们還會顯示如何從 HTTP header 中提取文件名,以 f 確保下載的文件命名正確。

Api 設定

要記得設置告诉 axios 期望接收的類型為 blob,若是不設置,則會預設為 json,收到的資料會是亂碼。

1
2
3
4
5
6
7
8

exportFile(searchData) {
return axios.get("/data/export", {
params: searchData,
responseType: "blob"
});
}

前端處理文件下載

獲取到後端返回的文件數據後,我們需要在前端將其轉換為可下載的文件格式。以下是一個完整的示例,展示瞭如何將 blob 數據生成 Excel 文件並觸發下載。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const handleExportFile = async () => {
try {
const res = await exportFile(data);

const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});

const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;

const fileName = "export.xlsx";
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
link.remove();

window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error(error);
}
};

new Blob([res.data], { type: … }) 做了什麼?

這里,用 new Blob() 創建了一個 Blob 對象。
res.data 是從後端 API 接收到的文件數據。
我們將這個數據放入 Blob 中,同時指定了文件的類型,即 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,這是 Excel 文件的 MIME 類型。這樣瀏覽器就能識別出這是一個 Excel 文件。

動態創建一個 <a> 標簽,並通過模擬點擊觸發文件下載。

使用 window.URL.createObjectURL(blob) 創建了一個特殊的 URL,它指向我們剛才創建的 Blob 對象。
可以理解為它生成了一個臨時的下載地址,使用者可以透過這個地址下載文件。
接著 document.createElement(“a”) 創建 <a> 標籤,接下來要利用這個標籤來實現文件下載。
也就是 link.href = downloadUrl; 給 <a> 標籤賦值,將剛才生成的 downloadUrl 設定為這個 <a> 標籤的 link 地址 (href),也就是說,點擊這個 link 會指向我們的 Blob 物件(即文件資料)。

設定文件名並觸發下載

使用 setAttribute 方法給<a> 標籤添加一個 download 屬性,並設定文件名為 “export.xlsx”。這樣,當用戶點擊鏈接時,瀏覽器會提示下載文件,並自動將文件保存為 export.xlsx。
document.body.appendChild 將 <a> 標籤臨時加到 document 的 body 中。雖然使用者不會看到這個 link,但它在頁面上是存在的。
然後模擬一次使用者點擊這個連結 (link.click()),觸發文件的下載過程。
link.remove();下載操作完成後,我們把這個臨時創建的 <a> 標籤從頁面中移除。

Read more »

在現代網頁應用中,強大的文字編輯器是必不可少的。Tiptap 是一款基於 ProseMirror 的高擴展性編輯器。本文將介紹如何在 Vue3 中使用 Tiptap,並結合 Vuetify 實現文字編輯器。

官網文件

首先官網:https://tiptap.dev/docs/editor/getting-started/install/vue3

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<editor-content :editor="editor" />
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
content: '<p>I’m running Tiptap with Vue.js. 🎉</p>',
extensions: [StarterKit],
})
</script>

根據對應 EXTENSIONS 找出如何加入在編輯器,如:bold, italic

1
2
3
4
5
6
     <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
Toggle bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
Toggle italic
</button>
Read more »

最近嘗試在專案內加入測試,詢問朋友後建議先從 utils 中共用的邏輯函式開始,而在共用的邏輯函式中,會有幾個是 i18n 相關的函式。
因為頁面中常會用到將數值,轉換為顯示在畫面上的文字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
const getType = (type: number) => {
switch (type) {
case 0:
return t('Type.Auto')
case 1:
return t('Type.Manual')
case 2:
return t('Type.Mix')
default:
return null
}
}

在進行測試時,總想說是否要先選定一語系,如中文\英文,然後確認他轉換的是否為該文字
但是經過嘗試後,覺得或許不應該是檢查轉換後的文字,應該要以對應 key 來檢查。
參考 overflow

  • 因此專案是使用 quasar,所以在 i18n.t 方法,引入的位置是 boot/i18n.ts
  • 在測試中,可以執行測試函式,帶入對應數值,並檢查是否回傳對應的 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// 在測試中覆寫模擬 i18n.t 方法
vi.mock('../../../src/boot/i18n', () => ({
i18n: {
global: {
t: (key: string) => key, // 提供模擬的 t 函式
locale: 'en' // 假設默認的 locale 值
}
}
}))

describe('getType', () => {
it('should return the correct string for type 0', () => {
expect(getTradingType(0)).toBe('Type.Auto')
})

it('should return the correct string for type 1', () => {
expect(getTradingType(1)).toBe('Type.Manual')
})

it('should return the correct string for type 2', () => {
expect(getTradingType(2)).toBe('Type.Mix')
})
it('should return null for an unknown type', () => {
expect(getTradingType(999)).toBeNull()
})
})

小結上述方式:

使用全局模擬,vi.mock 全局模擬 i18n.t 方法,並提供模擬的 t 函式,這樣就可以在測試中,直接檢查是否回傳對應的 key,而不用檢查轉換後的文字。

另外,在跨頁面也會有共用的下拉選單,將此下拉選單的選項,也提取出來,並進行測試。

1
2
3
4
5
6

const directionOpt = computed(() => ([
{ label: t('Shared.right'), value: 0 },
{ label: t('Shared.wrong'), value: 1 }
]))

原本也想是否直接檢查 key 是否正確即可。但基於困惑,就嘗試丟 chatGpt 詢問

Read more »

在 Youtube 看到 Kevin Powell 介紹使用 grid 來作為 wrapper,以自適應網頁的縮放。

首先基本常用 grid 方式

  • 創建一個包含三欄等寬的網格布局
1
2
3
4
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}

加入使用 using named grid lines,定義出欄的 content 範圍

1
2
3
4
.content-grid {
display: grid;
grid-template-columns: 1fr [content-start] 1fr [content-end] 1fr;
}

設計一個 class 給他,如果在 content-grid 以下的元素,會將以下的元素放在 content 之間

  • 應用到 .content-grid 內部的每一個直接子元素
1
2
3
4
5
6
7
8
.content-grid {
display: grid;
grid-template-columns: 1fr [content-start] 1fr [content- end] 1fr;
}

.content-grid > * {
grid-column: content;
}

Layout grid 可以先設置:

  • 就可以看到 content 範圍
    image
    image
Read more »

因工作需求需要製作簽名面板,查看套件使用度較高 signature_pad

  1. 在專案內安裝 signature_pad 套件 npm i signature_pad
  2. 嘗試將引入的簽名套件包成可以重複使用的元件。

initializeSignaturePad : 進行初始化,如尺寸大小、筆的顏色、畫布背景色等。這邊將這些設置放在 props 讓使用時可以保有一些彈性空間。
resizeCanvas : 用於調整畫布的大小,目的是確保畫布的大小能夠適應不同的設備和螢幕解析度。
saveSignature\ clearSignature: 顧名思義就是儲存、清除簽名
注意:因為將簽名功能包成元件,使用 emit 將儲存的簽名傳到外層。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<div
class="signature-pad"
:style="{ width: props.width, height: props.height }"
>
<canvas ref="canvas"></canvas>
<slot>
<button @click="clearSignature">清除簽名</button>
<button @click="saveSignature">儲存簽名</button>
</slot>
</div>
</template>

<script setup>
import { onMounted, ref, watch, onUnmounted } from "vue";
import SignaturePad from "signature_pad";

const props = defineProps({
width: { type: String, default: "100%" },
height: { type: String, default: "300px" },
penColor: { type: String, default: "black" },
backgroundColor: { type: String, default: "white" },
options: { type: Object, default: () => ({}) },
});

const canvas = ref(null);
let signaturePad = null;

const initializeSignaturePad = () => {
if (canvas.value) {
signaturePad = new SignaturePad(canvas.value, {
...props.options,
penColor: props.penColor,
backgroundColor: props.backgroundColor,
});
}
};

const resizeCanvas = () => {
//window.devicePixelRatio 來獲取設備的像素密度,如果無法獲取到,則將ratio設為1。
const ratio = Math.max(window.devicePixelRatio || 1, 1);
//根據畫布元素的寬度和高度,乘以ratio,來設定畫布的寬度和高度。為了確保畫布在高像素密度的設備上顯示正確。
canvas.value.width = canvas.value.offsetWidth * ratio;
canvas.value.height = canvas.value.offsetHeight * ratio;
//獲取畫布的2D繪圖上下文,並使用scale方法將繪圖上下文的縮放比例設為ratio。
canvas.value.getContext("2d").scale(ratio, ratio);
signaturePad.clear(); // Clears any existing drawing
};

const emit = defineEmits(["clear", "save"]);

const saveSignature = () => {
if (signaturePad) {
const signatureImage = signaturePad.toDataURL();
emit("save", signatureImage);
}
return null;
};

const clearSignature = () => {
if (signaturePad) {
signaturePad.clear();
emit("clear"); // 發射清除事件
}
};

// Initialize and resize canvas
onMounted(() => {
initializeSignaturePad();
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
});

onUnmounted(() => {
window.removeEventListener("resize", resizeCanvas);
});

// Watch for prop changes
watch(
() => [props.penColor, props.backgroundColor, props.options],
() => {
initializeSignaturePad();
},
{ deep: true }
);
</script>

參考資料:

https://github.com/WangShayne/vue3-signature

延續上次介紹常用的 Vue test util 語法,今天要再多介紹幾個。

.text().html()

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="hello">
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">
vue-cli documentation
</a>
</p>
</div>
</template>

使用 html 若直接 console 出會將 template 呈現出來

1
2
3
html=> <div class="hello">
<p> For a guide and recipes on how to configure / customize this project,<br> check out the <a href="https://cli.vuejs.org" target="_blank" rel="noopener"> vue-cli documentation </a></p>
</div>

在測試當中,可以直接抓取該斷落的內容進行比對

  1. 取得該段的 html
  2. 取得該 DOM 元素,並進行 html 的比對
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "./HelloWorld.vue";

describe("HelloWorld.vue", () => {
it("html & text", () => {
const wrapper = shallowMount(HelloWorld);
// console.log("html=>", wrapper.html());
// console.log("text=>", wrapper.text());
expect(wrapper.html()).toMatch(
`<a href="https://cli.vuejs.org" target="_blank" rel="noopener"> vue-cli documentation </a>`);
console.log("aLink_>", wrapper.find('a').html())
expect(wrapper.find('a').html()).toMatch(
`<a href="https://cli.vuejs.org" target="_blank" rel="noopener"> vue-cli documentation </a>`);
});
});

component 的簡易測試

如課程範例為一個卡片,裡面有一個圖片元件(ImageBox)、內文元件(Content)

1
2
3
4
5
6
<template>
<div id="CardBox">
<ImageBox />
<Content v-if="isOpenContent" />
</div>
</template>

test goal: 檢查 component 是否有隨狀態而成功渲染與否。

findComponent

判斷 component 是否有存在
首先要如何抓元件?

  1. 因頁面中是掛載組件,所以這邊使用 mount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { mount } from "@vue/test-utils";
import CardBox from "./index.vue";
import ImageBox from "./ImageBox.vue";
import Content from "./Content.vue";

describe("CardBox.vue", () => {
it("findComponent function test", () => {
const wrapper = mount(CardBox);

expect(wrapper.findComponent(ImageBox).exists()).toBe(true);
expect(wrapper.findComponent(Content).exists()).toBe(true);
});
});

  • findComponent(‘.class’)
  • findComponent({name: a}) 組建名稱
  • findComponent({ref: ‘ref’}) 組件實體:綁在 DOM 上的 ref
  • findComponent(component) 取得組件:抓取導入的組件

findAllComponents

  • 此方式是會回傳陣列,要使用 at()
  • 不支援 ref 查找
1
expect(wrapper.findAllComponents(ImageBox).at(0).exists()).toBe(true);
1
Using find to search for a Component is deprecated and will be removed. Use findComponent instead. The find method will continue to work for finding elements using any valid selector(tag selectors (div, foo, bar), attribute selectors ([foo], [foo="bar"])...).

測試 class and attribute

可以使用在狀態的切換或是驗證資料的對錯,可以用 class 存在與否驗證。

attribute

  • 資料在渲染的時候,是否有正確的塞入到 attribute。
  • 在做資料的渲然,將拿到的內容塞入 attribute 中,如 a link 的 href, img src

範例題目延續上述使用得內文元件(Content)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
import { ref } from "@vue/reactivity";

export default {
props: {
styleColor: {
type: String,
default: "red",
},
},
setup(props) {
return {
props,
};
},
};
</script>

<template>
<div class="content">
<h1 :class="['card_title', props.styleColor]">
Vue vue-test-utils
</h1>
<a href="javascript:;">click</a>
<p class="card_text">
Vue Test Utils (VTU) is a set of utility functions aimed to simplify testing Vue.js components. It provides some methods to mount and interact with Vue components in an isolated manner!
</p>
</div>
</template>

test goal: 測試 <h1> 有沒有存在 styleColor

  • 使用 class() 帶入參數,就會去檢查該 class 是否存在
    測試架構:
  1. 先確定該元件有被渲染出來
  2. 找到 h1 tag 並查看是否有 styleColor 傳入
  3. 元件內的 a link 放入 href
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { shallowMount } from "@vue/test-utils";
import Content from "./Content.vue";

describe("Content.vue", () => {
it("find component",()=>{
const wrapper = shallowMount(Content);

expect(wrapper.find('.card_title').exists()).toBe(true)
})
it("check h1 tag have red class", ()=>{
const wrapper = shallowMount(Content);

expect(wrapper.find('h1').classes('red')).toBe(true);
})
it("button is disabled attr", () => {
const wrapper = shallowMount(Content);
console.log('red', wrapper.find('a').attributes("href"))
expect(wrapper.find('a').attributes("href")).toBe("javascript:;");
})
});

練習方式,聽完講師分享的操作之後,寫下測試目標的結構,就可以嘗試自己撰寫看看!

檢查是否可見 isVisible & exists

前面的範例練習,就可以看檔有使用 exists
在 vue template 中,有使用 v-if, v-show 來處理 div 顯示與否。
v-show 會存在於 DOM 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { shallowMount } from "@vue/test-utils";
import PhotoItem from "./index.vue";

describe("PhotoItem.vue", () => {
it("DOM is v-if or v-show hide", () => {
const wrapper = shallowMount(PhotoItem);

// exists 用來查看是否存在
expect(wrapper.find("#box1").exists()).toBe(false);
// isVisible 用來判斷DOM是否給 v-show 隱藏起來
expect(wrapper.find("#box2").isVisible()).toBe(false);
});

});

implicit assertion(隱含斷言)

常用的 should 關鍵字:

  1. be.visible:確保元素在畫面上可見。
  2. be.hidden:確保元素在畫面上隱藏。
  3. be.checked:確保複選框或單選框元素被選中。
  4. be.disabled:確保元素被禁用。
  5. have.text:檢查元素的文字內容是否符合預期。
  6. have.value:檢查輸入元素的值是否符合預期。
  7. have.attr:檢查元素的特定屬性值是否符合預期。
  8. have.class:檢查元素是否具有特定的類名。
  9. contain:檢查元素是否包含指定的文字內容。

可根據具體需求進行選擇和使用
keywords

這裡的練習範例,驗證當前網址(URL)的斷言方法。

include:確保當前網址包含特定的子字串。

1
2
cy.url().should('include', '/login');

eq:確保當前網址與預期值完全相等。

1
2
cy.url().should('eq', 'https://example.com/dashboard');

contain:檢查當前網址是否包含特定的字串。

1
cy.url().should('contain', 'example.com');

這些斷言方法可用於驗證當前網址是否符合預期,從而確保導航或操作正確導致了預期的網址變化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe("Assertion practice",()=>{

it("explicit", ()=>{

cy.visit('https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')

//should + keyword end

cy.url().should('include', 'orangehrmlive.com')

cy.url().should('eq', 'https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')

cy.url().should('contain', 'orangehrmlive')


})

})

  • 可以看到上述,有連續使用到 url()
1
2
3
4
5
cy.url().should('include', 'orangehrmlive.com')

.should('eq', 'https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')

.should('contain', 'orangehrmlive')
1
2
3
4
5
cy.url().should('include', 'orangehrmlive.com')

.and('eq', 'https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')

.and('contain', 'orangehrmlive')

另外可以檢查進到頁面 logo 圖示的顯示

1
2
3
4
5
6
7
8
9
10

describe("Assertion practice",()=>{
it("explicit", ()=>{
cy.visit('https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')
//check logo 呈現與否
cy.get('.orangehrm-login-branding > img').should('be.visible')
.and('exist')
})
})

  • 使用者輸入框
1
2
3
4
5
6
7
8
9
10
11
12
13
14

describe("Assertion practice",()=>{
it("explicit", ()=>{
cy.visit('https://opensource-demo.orangehrmlive.com/web/index.php/auth/login')

//檢查 username 得輸入
//取得 username 的輸入框,並且確保輸入的值
cy.get("input[placeholder='Username']").type("Admin")
cy.get("input[placeholder='Username']").should("have.value", "Admin")


})
})

explicit assertion

(顯式斷言)」是指明確使用斷言方法來檢查特定的條件或預期結果。
與隱含斷言不同,顯式斷言需要您明確指定斷言方法來進行驗證。