線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
記錄雞蛋糕的每一步前端煉成過程,從小白到(也許是)前端工程師的學習與分享。
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
記錄雞蛋糕的每一步前端煉成過程,從小白到(也許是)前端工程師的學習與分享。

Subscribe to 雞蛋糕的前端修煉屋

Subscribe to 雞蛋糕的前端修煉屋
今日閱讀:《單元測試的藝術》3/e 第二章 2.4 p.46 - 2.5.9 p.56,並將範例程式碼改寫成 Vitest。
GitHub repo: https://github.com/gcake119/aout3-samples
用 verifyPassword 驗證密碼格式,練習「測試如何寫得清楚」。
verifyPassword(input, rules) 接收 rules(規則函式陣列),回傳 input 套用 rules 的結果。
每個 rule 回傳物件格式:{ passed: (boolean), reason: (string) }。
註:書中示範偏 test...expect 的精簡風格,我一開始用自己習慣的 describe...it...expect 階層式寫法,後面才理解作者是在刻意示範不同風格。

graph TB
subgraph "單元測試範圍"
Entry["🚪 進入點<br/>verifyPassword(input, rules)"]
subgraph WorkUnit["⚙️ 工作單元 (Unit of Work)"]
Process1["遍歷所有 rules"]
Process2["執行每個 rule(input)"]
Process3["收集失敗的 reasons"]
end
Exit1["✅ 退出點 1: 回傳值<br/>return errors"]
Exit2["⚠️ 退出點 2: 狀態改變<br/>(本例無)"]
Exit3["📞 退出點 3: 第三方調用<br/>(本例無)"]
end
Test["📝 測試程式碼"] -->|"輸入: 'any input', [fakeRule]"| Entry
Entry --> Process1
Process1 --> Process2
Process2 --> Process3
Process3 --> Exit1
Exit1 -.->|"檢查"| Assert1["expect(errors[0])<br/>.toMatch('fake reason')"]
Exit2 -.->|"檢查"| Assert2["例: expect(logger.log)<br/>.toHaveBeenCalled()"]
Exit3 -.->|"檢查"| Assert3["例: expect(db.save)<br/>.toHaveBeenCalledWith(...)"]
style Entry fill:#a8dadc
style WorkUnit fill:#f1faee
style Exit1 fill:#06d6a0
style Exit2 fill:#ddd,stroke-dasharray: 5 5
style Exit3 fill:#ddd,stroke-dasharray: 5 5
// password-verifier0.js
export const verifyPassword = (input, rules) => {
const errors = [];
rules.forEach((rule) => {
const result = rule(input);
if (!result.passed) {
errors.push(`error ${result.reason}`);
}
});
return errors;
};
// password-verifier0.test.js
import { describe, it, expect } from 'vitest';
import { verifyPassword } from './password-verifier0';
describe('Password Verifier', () => {
it('Badly named test', () => {
// Arrange - 設定測試輸入
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act - 用輸入呼叫進入點
const errors = verifyPassword('any input', [fakeRule]);
// Assert - 檢查退出點
expect(errors[0]).toMatch('fake reason');
});
});
測試結構可以用「Arrange-Act-Assert」三段式來看:Arrange 準備資料,Act 執行受測行為,Assert 驗證結果。這個結構能讓測試更易讀,也更容易維護 。 semaphore
如果不是在做 TDD,而是在功能完成後補測試,可以在測試通過後故意在原程式加入一個 bug,確認測試會在該失敗時失敗。這能快速檢查測試是否真的有守住需求 。 semaphore
USE 是我用來提醒自己「測試名稱應包含的三件事」:
unit under test:受測的工作單元
scenario:情境或輸入
expectation:期望的行為或退出點
describe() 當外層語意分組,必要時嵌套提高可讀性(常見被稱為 BDD 風格)。
it() 表達單一情境下的單一預期。
取決於複雜度與表達需求:
test...expect:精簡、直接
describe...it...expect:階層式、可讀性高
Vitest 支援 describe/it/expect 等 API,寫法與 Jest 相近 。 vitest
// password-verifier1.js
export class PasswordVerifier1 {
constructor() {
this.rules = [];
}
addRule(rule) {
this.rules.push(rule);
}
verify(input) {
if (this.rules.length === 0) {
throw new Error('There are no rules configured');
}
const errors = [];
this.rules.forEach((rule) => {
const result = rule(input);
if (result.passed === false) {
errors.push(result.reason);
}
});
return errors;
}
}
// password-verifier1.test.js
import { describe, it, expect } from 'vitest';
import { PasswordVerifier1 } from './password-verifier1';
describe('Password Verifier', () => {
describe('with a failing rule', () => {
it('has an error message based on the rule.reason', () => {
// Arrange
const verifier = new PasswordVerifier1();
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
// Assert
expect(errors[0]).toMatch('fake reason');
});
it('has exactly one error message', () => {
// Arrange
const verifier = new PasswordVerifier1();
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
// Assert
expect(errors.length).toBe(1);
});
});
});
Borda Count 是一種「排序投票的計分法」:選民對候選人排名,系統依排名給分,最後加總分數決定結果 。 www3.nd
第一順位:4 分
第二順位:3 分
第三順位:2 分
第四順位:1 分
# | 8票 | 2票 | 6票 | 4票 |
|---|---|---|---|---|
第一順位 | ① | ③ | ② | ④ |
第二順位 | ③ | ② | ③ | ③ |
第三順位 | ② | ④ | ④ | ② |
第四順位 | ④ | ① | ① | ① |
總票數:8 + 2 + 6 + 4 = 20 票
依「欄」分類:每欄代表一組相同排名的選票,票數不同。
再依「候選人」分類:每位候選人在每個順位拿到多少票。
依配分(4/3/2/1)乘上票數並加總,得到每位候選人總分 。 www3.nd
候選人① = 44 分
候選人② = 54 分
候選人③ = 62 分
候選人④ = 40 分
當選人:候選人③(最高分 62) www3.nd
正確計分
Given 完整投票資料表(4 組選票,4 位候選人)
When 執行 Borda Count 計算
Then 應正確輸出每位候選人的總分:①=44、②=54、③=62、④=40
正確回傳當選人
Given 已計算出所有候選人的總分
When 比較各候選人分數
Then 應回傳分數最高的候選人(③獲勝)
const ballots = [
{ count: 8, ranking: [1, 3, 2, 4] },
{ count: 2, ranking: [3, 2, 4, 1] },
{ count: 6, ranking: [2, 3, 4, 1] },
{ count: 4, ranking: [4, 3, 2, 1] },
];
ranking[0] = 第一順位候選人號次
count = 該排序的票數
{
1: [8, 0, 0, 12], // 候選人①:第1順位8票、第4順位12票
2: [6, 2, 12, 0], // 候選人②:第1順位6票、第2順位2票、第3順位12票
3: [2, 18, 0, 0], // 候選人③:第1順位2票、第2順位18票
4: [4, 0, 8, 8], // 候選人④:第1順位4票、第3順位8票、第4順位8票
}
陣列 index 對應順位(0=第1順位、1=第2順位…)
值是該候選人在該順位的總票數
輸入格式 接近人類閱讀的表格樣貌
中間格式 以候選人為維度,方便對每位候選人計分
取值直覺:candidateVotes [vitest](https://vitest.dev/guide/features) 就是候選人③的第二順位票數
函式:transformBallotsToCandidateVotes(ballots, numberOfCandidates)
流程:
初始化每位候選人的票數陣列為 Array(numberOfCandidates).fill(0)
走訪每張選票 { count, ranking }
對 ranking 中的每個候選人,在對應順位累加票數
函式:calculateBordaCount(votes)
流程:
定義配分陣列 [4, 3, 2, 1]
票數陣列與配分陣列同 index 相乘
reduce 加總得到總分
Array.from({ length: n }, (_, i) => i + 1):建立候選人清單
Array(n).fill(0):初始化每位候選人的票數陣列
forEach:做累加副作用(把票數加到 candidateVotes)
map:票數 × 配分的轉換
reduce((acc, cur) => acc + cur, 0):加總分數
在函式開頭檢查前置條件,不符合就直接 throw new Error(...),讓主流程保持乾淨 。 semaphore
if (numberOfCandidates !== ballots[0]?.ranking.length) {
throw new Error('Ranking length does not match number of candidates');
}
expect(() => transformBallotsToCandidateVotes(ballots, 4)).toThrow();
注意要用 () => 包住函式呼叫,否則錯誤會在 expect 外部就拋出,測試框架接不到。
今日閱讀:《單元測試的藝術》3/e 第二章 2.4 p.46 - 2.5.9 p.56,並將範例程式碼改寫成 Vitest。
GitHub repo: https://github.com/gcake119/aout3-samples
用 verifyPassword 驗證密碼格式,練習「測試如何寫得清楚」。
verifyPassword(input, rules) 接收 rules(規則函式陣列),回傳 input 套用 rules 的結果。
每個 rule 回傳物件格式:{ passed: (boolean), reason: (string) }。
註:書中示範偏 test...expect 的精簡風格,我一開始用自己習慣的 describe...it...expect 階層式寫法,後面才理解作者是在刻意示範不同風格。

graph TB
subgraph "單元測試範圍"
Entry["🚪 進入點<br/>verifyPassword(input, rules)"]
subgraph WorkUnit["⚙️ 工作單元 (Unit of Work)"]
Process1["遍歷所有 rules"]
Process2["執行每個 rule(input)"]
Process3["收集失敗的 reasons"]
end
Exit1["✅ 退出點 1: 回傳值<br/>return errors"]
Exit2["⚠️ 退出點 2: 狀態改變<br/>(本例無)"]
Exit3["📞 退出點 3: 第三方調用<br/>(本例無)"]
end
Test["📝 測試程式碼"] -->|"輸入: 'any input', [fakeRule]"| Entry
Entry --> Process1
Process1 --> Process2
Process2 --> Process3
Process3 --> Exit1
Exit1 -.->|"檢查"| Assert1["expect(errors[0])<br/>.toMatch('fake reason')"]
Exit2 -.->|"檢查"| Assert2["例: expect(logger.log)<br/>.toHaveBeenCalled()"]
Exit3 -.->|"檢查"| Assert3["例: expect(db.save)<br/>.toHaveBeenCalledWith(...)"]
style Entry fill:#a8dadc
style WorkUnit fill:#f1faee
style Exit1 fill:#06d6a0
style Exit2 fill:#ddd,stroke-dasharray: 5 5
style Exit3 fill:#ddd,stroke-dasharray: 5 5
// password-verifier0.js
export const verifyPassword = (input, rules) => {
const errors = [];
rules.forEach((rule) => {
const result = rule(input);
if (!result.passed) {
errors.push(`error ${result.reason}`);
}
});
return errors;
};
// password-verifier0.test.js
import { describe, it, expect } from 'vitest';
import { verifyPassword } from './password-verifier0';
describe('Password Verifier', () => {
it('Badly named test', () => {
// Arrange - 設定測試輸入
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act - 用輸入呼叫進入點
const errors = verifyPassword('any input', [fakeRule]);
// Assert - 檢查退出點
expect(errors[0]).toMatch('fake reason');
});
});
測試結構可以用「Arrange-Act-Assert」三段式來看:Arrange 準備資料,Act 執行受測行為,Assert 驗證結果。這個結構能讓測試更易讀,也更容易維護 。 semaphore
如果不是在做 TDD,而是在功能完成後補測試,可以在測試通過後故意在原程式加入一個 bug,確認測試會在該失敗時失敗。這能快速檢查測試是否真的有守住需求 。 semaphore
USE 是我用來提醒自己「測試名稱應包含的三件事」:
unit under test:受測的工作單元
scenario:情境或輸入
expectation:期望的行為或退出點
describe() 當外層語意分組,必要時嵌套提高可讀性(常見被稱為 BDD 風格)。
it() 表達單一情境下的單一預期。
取決於複雜度與表達需求:
test...expect:精簡、直接
describe...it...expect:階層式、可讀性高
Vitest 支援 describe/it/expect 等 API,寫法與 Jest 相近 。 vitest
// password-verifier1.js
export class PasswordVerifier1 {
constructor() {
this.rules = [];
}
addRule(rule) {
this.rules.push(rule);
}
verify(input) {
if (this.rules.length === 0) {
throw new Error('There are no rules configured');
}
const errors = [];
this.rules.forEach((rule) => {
const result = rule(input);
if (result.passed === false) {
errors.push(result.reason);
}
});
return errors;
}
}
// password-verifier1.test.js
import { describe, it, expect } from 'vitest';
import { PasswordVerifier1 } from './password-verifier1';
describe('Password Verifier', () => {
describe('with a failing rule', () => {
it('has an error message based on the rule.reason', () => {
// Arrange
const verifier = new PasswordVerifier1();
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
// Assert
expect(errors[0]).toMatch('fake reason');
});
it('has exactly one error message', () => {
// Arrange
const verifier = new PasswordVerifier1();
const fakeRule = (input) => ({ passed: false, reason: 'fake reason' });
// Act
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
// Assert
expect(errors.length).toBe(1);
});
});
});
Borda Count 是一種「排序投票的計分法」:選民對候選人排名,系統依排名給分,最後加總分數決定結果 。 www3.nd
第一順位:4 分
第二順位:3 分
第三順位:2 分
第四順位:1 分
# | 8票 | 2票 | 6票 | 4票 |
|---|---|---|---|---|
第一順位 | ① | ③ | ② | ④ |
第二順位 | ③ | ② | ③ | ③ |
第三順位 | ② | ④ | ④ | ② |
第四順位 | ④ | ① | ① | ① |
總票數:8 + 2 + 6 + 4 = 20 票
依「欄」分類:每欄代表一組相同排名的選票,票數不同。
再依「候選人」分類:每位候選人在每個順位拿到多少票。
依配分(4/3/2/1)乘上票數並加總,得到每位候選人總分 。 www3.nd
候選人① = 44 分
候選人② = 54 分
候選人③ = 62 分
候選人④ = 40 分
當選人:候選人③(最高分 62) www3.nd
正確計分
Given 完整投票資料表(4 組選票,4 位候選人)
When 執行 Borda Count 計算
Then 應正確輸出每位候選人的總分:①=44、②=54、③=62、④=40
正確回傳當選人
Given 已計算出所有候選人的總分
When 比較各候選人分數
Then 應回傳分數最高的候選人(③獲勝)
const ballots = [
{ count: 8, ranking: [1, 3, 2, 4] },
{ count: 2, ranking: [3, 2, 4, 1] },
{ count: 6, ranking: [2, 3, 4, 1] },
{ count: 4, ranking: [4, 3, 2, 1] },
];
ranking[0] = 第一順位候選人號次
count = 該排序的票數
{
1: [8, 0, 0, 12], // 候選人①:第1順位8票、第4順位12票
2: [6, 2, 12, 0], // 候選人②:第1順位6票、第2順位2票、第3順位12票
3: [2, 18, 0, 0], // 候選人③:第1順位2票、第2順位18票
4: [4, 0, 8, 8], // 候選人④:第1順位4票、第3順位8票、第4順位8票
}
陣列 index 對應順位(0=第1順位、1=第2順位…)
值是該候選人在該順位的總票數
輸入格式 接近人類閱讀的表格樣貌
中間格式 以候選人為維度,方便對每位候選人計分
取值直覺:candidateVotes [vitest](https://vitest.dev/guide/features) 就是候選人③的第二順位票數
函式:transformBallotsToCandidateVotes(ballots, numberOfCandidates)
流程:
初始化每位候選人的票數陣列為 Array(numberOfCandidates).fill(0)
走訪每張選票 { count, ranking }
對 ranking 中的每個候選人,在對應順位累加票數
函式:calculateBordaCount(votes)
流程:
定義配分陣列 [4, 3, 2, 1]
票數陣列與配分陣列同 index 相乘
reduce 加總得到總分
Array.from({ length: n }, (_, i) => i + 1):建立候選人清單
Array(n).fill(0):初始化每位候選人的票數陣列
forEach:做累加副作用(把票數加到 candidateVotes)
map:票數 × 配分的轉換
reduce((acc, cur) => acc + cur, 0):加總分數
在函式開頭檢查前置條件,不符合就直接 throw new Error(...),讓主流程保持乾淨 。 semaphore
if (numberOfCandidates !== ballots[0]?.ranking.length) {
throw new Error('Ranking length does not match number of candidates');
}
expect(() => transformBallotsToCandidateVotes(ballots, 4)).toThrow();
注意要用 () => 包住函式呼叫,否則錯誤會在 expect 外部就拋出,測試框架接不到。
<100 subscribers
<100 subscribers
Share Dialog
Share Dialog
No activity yet