Coder Social home page Coder Social logo

blog's Introduction

blog's People

Contributors

hsueh-jen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

魔王(Boss)設計原則😈

魔王(Boss)設計原則😈

從網路影片中整理出的魔王設計心得,很多元素不特定是用在Boss上,也適用在一般敵人。

預備動作 - Telegraphing

Telegraphing在Google翻譯上是電報的意思,Gamasutra網站裡面的文章Enemy Attacks and Telegraphing有不錯的解釋,但我還是沒找到合適的中文翻譯,我自己先翻成"預備動作"好了。

Telegraph (v) - To convey a message, intentionally or not, especially with facial expression or body language.中文意思是有意無意的釋出訊息,特別是肢體語言或是臉部語言

Telegraphing在這邊指的是Boss在出招前的預備動作,讓玩家知道要發生什麼事了,有時間做出反應。

Telegraphing可以明顯或不明顯,但不能完全讓人摸不著頭緒,比如說Boss要吐火前會有個吸氣的動作,讓玩家可以有時間反應,不然玩家會玩到腦羞。

有些攻擊力越強大的攻擊,Telegraphing的時間會越長,像是魔王在射出超大雷射光束前的集氣時間通常會比較久。

隨機

雖然魔王的攻擊有固定的行為模式,但如果每次出招的方式都一模一樣,會太容易被摸透,這樣玩起來會很無聊,所以在裡面加上隨機的要素很重要。

比如說火龍要吐火球的動作可以讓玩家察覺,但是會吐出一顆火球還是兩顆就不一定,或是火球是往左上方發射還左下發射也是未知的,這樣的隨機考驗玩家瞬間反應能力,所以玩家必須要專注去觀察火球的隨機方向與隨機速度,遊戲才會更有挑戰。

階段 - phase

這其實很常見,魔王在不同的階段會有不同的攻擊模式,如果你的魔王只有一個階段,你又想延長魔王戰的時間,有些人會把魔王血量調高,但那並非是好的設計,透過讓Boss有不同階段,可以讓延長魔王戰的時間,而且不失趣味。


相關影片精華整理

Good Game Design - Bosses

  1. boss 一定要有挑戰性,不能跟小兵一樣弱
  2. 要有一些讓玩家可預測的行為模式,不能讓人完全摸不著頭緒
  3. 給boss很多血不代表更有挑戰性更好玩,有時候只會讓遊戲更無聊
  4. boss最好是嚇人的,讓人心生畏懼的
  5. boss可以提供讓玩家驗收在這關學到的新技能,玩家希望自己不是靠暴力破關,而是靠批判性思考,最常見的就是薩爾達傳說
  6. 打完boss後會得到什麼? 通常最簡單的回報就是劇情的推進,但也可讓玩家得一些報酬,像是新武器
  7. boss不一定要長得很怪,但他的行為一定要跟一般敵人不同
  8. 如果不同的boss只是換不同的顏色或造型,玩家會覺得有點無聊,但是同樣的boss換不同角色去打可能會有不同的體驗
  9. boss可以有不同階段,也就是第二型態、第三型態的行為模式

【遊戲製作工具箱】《茶杯頭》中Boss是如何(試著)幹掉你

  1. 前搖Telegraphing,就是敵人出招前的動作
  2. 敵人攻擊有哪些套路,直的子彈,反彈子彈,S路線子彈(1:50)
  3. 魔王出招,如果有兩招,後期可以把兩招同時使出來(4:23)
  4. 魔王考驗了玩家
    • 記憶:記住魔王的招式有哪些
    • 反應:如果你全靠記憶背下魔王的套路,那很無聊,所以總要有點隨機,讓玩家反應
    • 技巧:就是玩家本身的手指靈活吧

Ptt GameDesign 板友對 Boss Design 的回饋

jerryklu:

要研究BOSS戰的設計我會推薦直接先從MGS全系列的BOSS戰看起,MGS真正唯一無人能及的就是BOSS戰的變化,你這些整理我猜基本上都是從日系風格遊戲來的,可以理解,但是在美系或是像MGS這種日美系混和風的有時候,例外就會有,雖然大原則還是差不多,但我想更理想的設計方式應該要強調你裡面寫到"考驗玩家技能"這個方向開始,因為隨機跟預備動作其實小兵也會有,只是程度的問題,還有另一個原因就是甚至要考量到需不需要有BOSS戰的問題,例如走寫實路線的FPS如COD或BF就不適合,另外一種是像起源以前的刺客教條,其實也不能算有BOSS戰,一個是他核心的機制不夠完備,不管是拳腳戰還是匿蹤,都不夠做出變化,另外一個是純潛行的BOSS戰本來就更難做,MGS大概有一半的BOSS戰跟潛行沒關係,甚至有些BOSS戰是幾乎全新的動作設定,所以簡單來說整個BOSS戰的根還是要先從你遊戲機制的核心多元度,再考慮整體風格,然後BOSS戰才有戲

LayerZ:

寫實路線FPS,可以直接看全境封鎖XD 可是後來就沒有什麼真的難的BOSS,FPS的難易度太難抓了,難起來一直被秒,全境後來是靠全域事件撐起來,各種秒本來很難過的王,那是後話,COD之前的BOSS戰與其說難易度,都是作氣氛的,但是這一代
BO4對於槍戰的打擊感總算抓到巧妙的平衡,不會過於秒殺,也不會打了沒感覺的FU,然後回你內文,你必須要把隨機性跟可預期性兩件事分開

ImCasual:

這說是Boss 不如說是會攻擊的敵人吧!我覺得Boss最重要的是一個「總結」感,讓人可以從遊戲的經驗當中找出這個Boss的打法,就會感覺到「我玩這個遊戲就是為了這刻」,動作遊戲就是 用你摸熟了的機制應對攻擊,劇情遊戲 重點會在讓你想起自己歷經過的事件,小Boss中Boss採用單純變化性大的沒問題 很有趣,不過最終Boss 必須把玩家投入的時間和情緒列入考慮,Undertale和平結局Boss戰超級感人,各種爆哭,但是那個戰鬥如果沒把遊戲打完就是個三洨,直接把玩家丟到那個結局 那個就不會有用,呃 總之我要說的是 Boss戰的設計 定義超廣,建議把標題限縮一下範圍

好好命名啊!同學! - Naming Convention

Naming Convention 指南

大家好,有一些 Naming Convention 的議題想跟各位分享與討論

勒布朗克法則(LeBlanc’s Law)説:Later equals Never
你心想著待會兒再回來整理程式,但其實永遠不會再回頭改的

最近想要整理出一些guideline跟同仁分享,想說這樣大家的程式可讀性提升,對團隊的合作會更好,不過我想先拋磚引玉,分享一些我整理出來的心得,希望大大們可以分享一下自己的 Naming Convention 或是有更好或更多的撰寫程式的規範跟原則。

參考書籍:


下方先是我整理的資料分享,歡迎大家也分享自己的 Naming Convention

1. Boolean變數或function的命名方式

Boolean變數或function的開頭必須使用is, can, has, should,但如果本身是形容就不需要,像是enabled, done就無需用is開頭

使用肯定的Boolean變數名稱。避免雙重否定:if (!notFound) { }。

2. 用介系詞(of,for,from,on...)來組變數名稱

這件事情但起來單純,但很多時候大家都會忽視介系詞硬組變數名稱,像是daysUntilDeadline就有可能被命名成DeadlineDays,如易造成誤解,以下是我找到一些不錯的範例

var daysSinceModification = 3; // 修改後已過了3天
var workDaysPerWeek = 5;       // 每週工作5天
var daysUntilDeadline = 10;     // deadline前還剩10天
var customersInOrder; // 已經排序過的客戶資料

3.使用更具體的單位以及用詞

delayedTime代表的是delay的時間,但其實單位是秒數,所以命名上最好使用更具體的單位來優化這個命名,改寫成delayedSeconds會更好

如果是一筆員工的資料,原先變數取名是person,那最好將person重新命名為employee。person的含義太寬泛,employee則能表示員工的概念。

4.捨棄i,j,k,使用更好的Iterator

一般for迴圈中我們都會使用到i, j, k,這其實不是不行,但是如果i, j, k分別代表parent, children, toy,那就可以用pi, ci, ti來取代i, j, k,這樣程式碼不會變長太多,可讀性提高也較容易debug。

不過更好的方式就是直接使用array.forEach()array.map(),會比iterator更清楚明瞭。

5.Function Name的prefix都是動詞

基本上function名稱全都是「動詞」開頭但如果像是getCellSize()calcCellSize(),可以直接省略開頭的動詞,直接使用cellSize()即可,不過也有人堅持不能用名詞開頭,基本上團隊統一即可。

6.縮寫真的方便但並不是最好

因為不想打太長的變數或函數名稱,大家總是在縮寫,但是有些縮寫很容易讓人搞混,對於原本就在維護這份程式的工程師或許還好,但新進工程師會很容易困惑。
使用像是dns,url這些眾所皆知的詞沒問題,只有自己團隊才看得懂的縮寫,除非必要,不然盡量避免使用。
不過我覺得這個看情況而定,重點是一定要有一份解釋縮寫的文件比較好。

7.如何寫註解

  • 在檔案的最前頭寫上整個程式檔案大致是如何運作的檔案註解
  • 除了解釋程式運作的原理外,還可以描述為什麼要用這樣子的寫法來寫,可能是效能上的需求之類的
  • 使用 TODO FIXME 等註解標籤,TODO可以標記尚未製作或是需要優化的部分,FIXME是不能運作需要修復的部分,其實我知道還有HACK, XXX 等標籤,不過我覺得好像都歸類在TODO比較方便,不知道大家的習慣是如何?
  • 在設定常數的時候給予註解其實也能幫助理解,讓開發者更有概念,像是下面程式註解說明為什麼要設定為1000的理由
// 加上合理的限制 - 沒有人能讀那麼多文章
const int MAX_RSS_SUBSCRIPTIONS = 1000;

8.封裝條件

如果if()裡面有超長一串的判斷式,可讀性會超差,所以可以把這個判斷式用boolean變數裝起來再判斷

boolean isMan = (user.age >= 18 && user.gender == 'male');

if (isMan){
    // 做一位男人該做的事>///<
}

其中最想請教大家的是function前方動詞的分類

Clean Code這本書上有說Pick one word for one abstract concept and stick with it.

不過比較沒人整理出較為通用的詞庫對應表,我知道這個本來就沒官方解答,但是我想聽聽看各位的經驗,整理出我自己的一套詞庫,以下是我目前整裡的詞庫

  • fetch(retrieve):從遠端(透過API)獲取資料,例如:fetchUsers()
  • load:從本地端加載資料,例如:loadFile()
  • calculate / calc|通過計算獲取資料,例如:calcBMI()
  • show:顯示物件,如showModal()、showDialog()
  • remove:將資料之間的關係移除,資料本身還是會存在
  • delete/destroy:將資料刪除,資料將會不存在
  • on:定義event的時候使用,像是onClick,onChange
  • handle:當onClick之類的event發生時所觸發的function,例如:handleClick,如果click後面有受詞的話,這將受詞移到click前方* 例如:handleButtonClick

剩下還有哪些詞是大家常用的呢?還請各位大大不吝分享,我在下面放一些可能會用到的詞,大家是怎麼給這些詞定義他專屬的concept,或是有哪些沒提到的也可以補充一下,我已經看過微軟 PowerShell 指令動詞列表了,不過想說如果大家有更具體的應用,也可以分享一下,麻煩大家給點意見了

get/set
create: insert append add append
edit: modify update
complete: finish done end
send: deliver, dispatch, announce, distribute, route
find: search, extract, locate, recover
start: launch, create, begin, open
make: create, set, build, genernate, compose, add, new

參考資料:小酌重構系列[18]—重命名

你聽過巨集(Macros)嗎?

VS Code - Macros巨集套件教學

這份文件會告訴你如何在VS Code中使用Macros,首先你要先安裝Macros套件,然後設定2~3個json檔即能使用Macros。

設定 settings.json

{
    "macros": {
      "makeAction": [
        "editor.action.addSelectionToNextFindMatch",
        "editor.action.clipboardCopyAction",
        "cursorEnd",
        {
          "command": "type",
          "args": {
            "text": ":wrapper.instance()."
          }
        },
        "editor.action.clipboardPasteAction",
        {
          "command": "type",
          "args": {
            "text": ","
          }
        }
      ],
        "makeFunctionTest": [
          "editor.action.addSelectionToNextFindMatch",
          {
            "command": "editor.action.insertSnippet",
            "when": "editorTextFocus",
            "args": {
              "name": "functionName2testCase"
            }
          }
        ]
    }
}

安裝完macros套件後,就能在setting.json中加入macros這個屬性,這次實作了makeAction與makeFunctionTest兩個macros command。

makeAction

makeAction功用:將任意一個單字word變成word: wrapper.instance().word,

  • editor.action.addSelectionToNextFindMatch:選取離游標最近的一個單字,預設快捷鍵是command+D
  • editor.action.clipboardCopyAction:複製
  • cursorEnd:將游標移至行末,也就是最右邊
  • type:輸入文字,輸入的內容填在args中的text
  • editor.action.clipboardPasteAction:貼上,也就是貼上剛剛複製的內容
  • 最後用剛剛學到的type補上一個逗號

makeFunctionTest

makeFunctionTest功用:將任意一個單字word變成下方程式碼

it('word()', () => {
 const { actions } =   setup();
 // if you want to mock word(), then use actions.mock('word');
 //actions.word();
 // TODO: check the result correct or not
});
  • editor.action.addSelectionToNextFindMatch:選取離游標最近的一個單字,預設快捷鍵是command+D
  • editor.action.insertSnippet:插入程式碼片段(Snippet),在本次範例中我使用自己撰寫的snippetfunctionName2testCase ,請參考下方的javascript.json

鍵盤快速鍵 keybindings.json

[
  {
    "key": "ctrl+a",
    "command": "macros.makeAction"
  },
  {
    "key": "ctrl+s",
    "command": "macros.makeFunctionTest",
  }
]

把剛剛在setting.json中設定好的macros command配上對應的快捷鍵。

使用者程式碼片段 javascript.json

{
    "functionName2testCase": {
        "prefix": "f2t",
        "body": [
            "it('$TM_SELECTED_TEXT($0)', () => {",
            "  const { actions } = setup();",
            "  // if you want to mock $TM_SELECTED_TEXT($0), then use actions.mock('$TM_SELECTED_TEXT');",
            "  // actions.$TM_SELECTED_TEXT($0);",
            "  // TODO: check the result correct or not",
            "});"
        ],
        "description": "description of functionName2testCase"
    }
}

如果還沒使用過自訂snippet功能的可以看這邊

剛剛使用editor.action.insertSnippet要插入的snippet如上,body就是實際會被插入的程式碼,其中要注意的是$TM_SELECTED_TEXT$0

  • $TM_SELECTED_TEXT:這是一個保留字,代表正被游標選取的文字,假如說我剛剛選取了一段程式片段word,接著插入snippet,此時的$TM_SELECTED_TEXT就是word

  • $0:在插入sinppet後,游標會停留在$0的位置,如果有多個$0,就會有多個游標,可以讓你輸入客製化的內容,除了$0之外還有$1,$2,$3以此類推,輸入完$0後就會跳到$1讓你繼續輸入。


補充

為了讓console.log()更加方便使用,我製作了speedConsoleLog,將任意一個單字word變成下方程式碼

console.log('word');
console.log(word);

settings.json

{
    "macros": 
        "speedConsoleLog": [
            "editor.action.copyLinesUpAction",
            "cursorHome",
            {
                "command": "type",
                "args": {
                    "text": "console.log('"
                }
            },
            "cursorEnd",
            {
                "command": "type",
                "args": {
                    "text": "');"
                }
            },
            "cursorDown",
            "cursorHome",
            {
                "command": "type",
                "args": {
                    "text": "console.log("
                }
            },
            "cursorEnd",
            {
                "command": "type",
                "args": {
                    "text": ");"
                }
            },
        ]
}

驗證資料的好幫手 - JSON Schema Validator

資料驗證好幫手 - JSON Schema Validator

文件大綱:

JSON schema 簡介及作用

直接先看一下JSON schema的範例,暖個身

{
   "title":"產品資訊",
   "description":"產品包含商品標號、商品名稱及價錢",
   "type":"object",
   "properties":{
      "pid":{
         "description":"產品的id,是product id的縮寫",
         "type":"integer"
      },
      "name":{
         "description":"產品名稱",
         "type":"string",
         "maxLength": 10
      },
      "price":{
         "type":"number",
         "exclusiveMinimum":0
      }
   },
   "required":[
      "pid",
      "name",
      "price"
   ]
}

JSON Schema就是JSON資料結構的規範,規定一個JSON資料該有哪些資料,包含規定型態、大小範圍等。JSON Schema有官方規範,可以在http://json-schema.org上查看,目前已出到draft-07。

這份文件要介紹的 Ajv (Another JSON Schema Validator) 套件會支援 draft-04/06/07 這幾個版本。

JSON Schema 功用如下:

  • 用於構建人機可讀的文件:
    • JSON Schema 可以讓系統讀取,同時也是一個讓人一目瞭然的文件檔,一舉兩得。
  • 用於生成模擬資料:
    • 有了JSON Schema,可以自動生成符合規定的資料讓測試程式使用。
  • 用於資料驗證:
    • 不需要再寫程式來判斷資料長度跟內容了,所有的邏輯都可以移植到JSON Schema中維護,最後我們會使用ajv這個工具來使用JSON schema進行驗證。

JSON schema 詳細介紹

網路上已經有文件把9成的功能介紹完了,廢話不多說直接看JSON Schema 辭典吧!

其他教學文件:


JSON Schema範例

比起看文件,大家更愛範例,所以在這邊提供幾個範例,剩下的可以去上面的文件看。

資料是數字(在這邊成為n), 3 <= n < 9 且 n要是3的倍數

{
  "type": "number",
  "minimum": 3, 
  "exclusiveMaximum": 9,
  "multipleOf": 3 
}
  • exclusiveMinimumexclusiveMaximum在draft-04以前是boolean,但在draft-06以後則變成數字。

字串長度為5~10

{
  "type": "string",
  "minLength": 5, 
  "maxLength": 10,
}

符合正規表示式的字串,輸入的字串必須符合身分證字號的格式

{
  "title": "ID card number",
  "type": "string",
  "pattern": "^[A-Z]{1}[0-9]{9}$"
}

符合ipv4的的字串,還有emaildate-time等format

{
  "type": "string",
  "format":"ipv4"
}

陣列長度2~5,而且必須都是數字,並不得有重複的數字

{
  "type": "array",
  "items": {
    "type": "number"
  },
  "minItems": 2,
  "maxItems": 5,
  "uniqueItems": true
}

陣列內只能有指定的[ 整數 , DC或Marvel ],不可以有其他東西

{
    "type": "array",
    "items": [
        {
            "description": "粉絲的年紀",
            "type": "integer"
        },
        {
          "type": "string",
          "enum": ["DC", "Marvel"]
        }
    ],
    "additionalItems": false
}
  • 在沒有additionalItems的情況下,[ 整數 , DC或Marvel , 這邊要放多少東西都可以 ],additionalItems可以指定type等相關資訊,代表多出來的陣列內容必須符合additionalItems的限制。

Object物件中,最多只能有5個property,一定要有nickname,當有carMileage時,就一定要有carBrand,反之則不需要,carBrand只能填bmw或benz,除了原本定義的property之外,其他的property都得是string型態。

{
   "type":"object",
   "maxProperties":5,
   "properties":{
      "nickname":{ "type": "string" },
      "carBrand":{ "enum": ["benz", "bmw"] },
      "carMileage":{ "type":"number" }
   },
   "required": ["nickname"],
   "dependencies":{ "carMileage": ["carBrand"] },
   "additionalProperties":{ "type":"string" }
}

oneOf代表 只能 符合其中一項,也就是這個數字必須是5或3的倍數,但不能是15的倍數,另外還有anyOfallOf以及not可以使用

{
    "type": "number",
    "oneOf": [
        { "multipleOf": 5 },
        { "multipleOf": 3 }
    ]
}

使用refs引用定義好的definitions,先定義好正整數後直接引用

{
    "type": "array",
    "items": { "$ref": "#/definitions/positiveInteger" },
    "definitions": {
        "positiveInteger": {
            "type": "integer",
            "exclusiveMinimum": 0
        }
    }
}

definitions相當於引用自己建立的變數的感覺,厲害的是$refs可以引用外部檔案的schema,像是json-schema.org提供的官方範例geo.json

{ 
    "$ref": "http://json-schema.org/geo.json#"
}

draft-04更新到draft-06時多出的功能

  • contains - { "contains": { "type": "integer" } } means that any array with at least one integer, any non-array is matched
  • propertyNames - property名稱必須符合規範
  • const - 比對不同property的值是否相同

draft-06更新到draft-07時多出的功能

if-else的範例

{
    "type": "integer",
    "minimum": 1,
    "maximum": 1000,
    "if": { "minimum": 100 },
    "then": { "multipleOf": 100 },
    "else": {
        "if": { "minimum": 10 },
        "then": { "multipleOf": 10 }
    }
}

符合規定的資料: 1, 5, 10, 20, 50, 100, 200, 500, 1000

不符合規定的資料:

  • -1, 0 (<1)
  • 2000 (>1000)
  • 11, 57, 123 (any number with more than one non-zero digit)
  • non-integers

$schema與$id到底是什麼?

The "$schema" keyword

  • $schema代表你寫的JSON Schema文件遵循的規範是哪一個版本。最新版的draft的schema是http://json-schema.org/schema#,目前如果你打開來看的話會是draft-07,如果想指定成draft-06版的話可以寫成http://json-schema.org/draft-06/schema#

The "$id" keyword - schemaId

  • 官方文件提到的schemaId
  • $id 是拿來定義這個schema的獨立編號,必須是uri。
    http://json-schema.org/example/geo.json#中的$id就是"http://json-schema.org/geo",別人使用$ref想遠端過來取用這個schema就是對應這個$id,object中的property也可以有$id,有如path的概念
  • 來看以下範例,如果想取用內部的定義則用#/definitions/B,取用外部的話則用http://example.com/other.json,詳細教學看這邊
   {
       "$id": "http://example.com/root.json",
       "definitions": {
           "A": { "$id": "#foo" },
           "B": {
               "$id": "other.json",
               "definitions": {
                   "X": { "$id": "#bar" },
                   "Y": { "$id": "t/inner.json" }
               }
           },
           "C": {
               "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"
           }
       }
   }
   

JSON Schema Validator

JSON Schema Validator就是專門來驗證JSON的工具,將要比對的資料拿去跟schema比對,馬上就知道資料符不符合規定,並且能夠得知是哪個資料的哪個環節不符合規定,相當方便,目前星星數最多,而且還有在維護的就屬Ajv(Another JSON Schema Validator)莫屬了,以前我用過的JSON Schema Validator是tv4

另外有一個react-jsonschema-form,只要在<form>物件中放入schema跟data,它就能自動生成Bootstrap表單,它的介紹語是這樣寫的"A simple React component capable of building HTML forms out of a JSON schema and using Bootstrap semantics by default."

react-jsonschema-form 程式範例

import React, { Component } from "react";
import { render } from "react-dom";

import Form from "react-jsonschema-form";

const schema = {
  title: "Todo",
  type: "object",
  required: ["title"],
  properties: {
    title: {type: "string", title: "Title", default: "A new task"},
    done: {type: "boolean", title: "Done?", default: false}
  }
};

const log = (type) => console.log.bind(console, type);

render((
  <Form schema={schema}
        onChange={log("changed")}
        onSubmit={log("submitted")}
        onError={log("errors")} />
), document.getElementById("app"));

但因為綁定Bootstrap有一點死,所以這邊還是選用比較有彈性的ajv。


Ajv: Another JSON Schema Validator

注意:完整的文件可以直接去github上看,這邊會提到比較常用到的內容,或是我有用過的內容。[npm ajv 連結]

這邊有模擬在Nodejs上面執行ajv的連結,可以進去試用看看。

我們已經學會了JSON Schema了,現在直接使用Ajv來比對schema跟data就能驗證資料了,看看下面的範例吧!符合條件的話ajv.validate就會回傳true,反之則false

var ajv = Ajv();
var schema = {
    "type": "number",
    "oneOf": [
        { "multipleOf": 5 },
        { "multipleOf": 3 }
    ]
};
var data10 = 10;
var data15 = 15;

console.log(ajv.validate(schema, data10)); // true 
console.log(ajv.validate(schema, data15)); // false

如果想知道錯在哪裡,我們就輸出ajv.errors,也是直接看下面範例。

我們在字串長度應該是5的schema中輸入長度是4的字串

var ajv = Ajv();
var schema = {
  "type": "string",
  "minLength": 5
};
var data = "four";

var valid = ajv.validate(schema, data);
if (!valid) console.log(ajv.errors);

資料長度不符合規定時Ajv回傳的錯誤訊息格式如下,大致上看得懂,我們去看下個範例後再解說這些參數是幹嘛的。

[
   {
      "keyword":"minLength",
      "dataPath":"",
      "schemaPath":"#/minLength",
      "params":{
         "limit":5
      },
      "message":"should NOT be shorter than 5 characters"
   }
]

這次的範例有兩個錯誤,分別是2016-12-99不符合date格式,以及fruit欄位應該要是字串卻填入了數字,如果要顯示超過一個以上的error就必須在Options欄位加上{ allErrors: true },否則它只會回傳第一個error。

var ajv = new Ajv({ allErrors: true });
var schema = {
    "type": "object",
    "properties": {
        "purchaseDate": { "format": "date" },
        "foods": {
            "type": "object",
            "properties": {
                "fruit": { "type": "string" }
            },
        },
    }
};
var data = {
    "purchaseDate": "2016-12-99",
    "foods": {
        "fruit": 5566
    }
};

var valid = ajv.validate(schema, data);
if (!valid) console.log(ajv.errors);

回傳了兩個錯誤

[
   {
      "keyword":"format",
      "dataPath":".purchaseDate",
      "schemaPath":"#/properties/purchaseDate/format",
      "params":{
         "format":"date"
      },
      "message":"should match format 'date'"
   },
   {
      "keyword":"type",
      "dataPath":".foods.fruit",
      "schemaPath":"#/properties/foods/properties/fruit/type",
      "params":{
         "type":"string"
      },
      "message":"should be string"
   }
]

想要知道error object的架構,可以看Validation errors,我把裡面的部分內容節錄在下方。

  • keyword: validation keyword.
  • dataPath: the path to the part of the data that was validated. By default dataPath uses JavaScript property access notation (e.g., ".prop[1].subProp"). When the option jsonPointers is true (see Options) dataPath will be set using JSON pointer standard (e.g., "/prop/1/subProp").
  • schemaPath: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation.
  • params: the object with the additional information about error that can be used to create custom error messages. [params文件]
  • message: the standard error message (can be excluded with option messages set to false).

API列表 - 接在ajv後面的api都寫在這。

.compile(Object schema)

validating function and cache the compiled schema for future use,比起每次都用ajv.validate()的執行速度還快,官方文件也說這是The fastest validation call


.errorsText([Array<Object> errors [, Object options]])

ajv.errorsText(ajv.errors)這樣的寫法可以把error object轉換成一個錯誤訊息字串,
remote schemas have to be added with addSchema or compiled to be available


.addFormat(String name, String|RegExp|Function|Object format)

使用addFormat定義客製化的format,可以參考下面的範例

var ajv = new Ajv().addFormat('cellphone', '^09[0-9]{2}-[0-9]{3}-[0-9]{3}$');

var schema = {
    "format": "cellphone"
};
var data = '0912-345-678'; // format have to be 09XX-XXX-XXX

var validate = ajv.compile(schema);
console.log(validate(data)); // true

.addKeyword(String keyword, Object definition)

自訂keyword

ajv.addKeyword('range', {
  type: 'number',
  compile: function (sch, parentSchema) {
    var min = sch[0];
    var max = sch[1];

    return parentSchema.exclusiveRange === true
            ? function (data) { return data > min && data < max; }
            : function (data) { return data >= min && data <= max; }
  }
});

var schema = { "range": [2, 4], "exclusiveRange": true };
var validate = ajv.compile(schema);
console.log(validate(2.01)); // true
console.log(validate(3.99)); // true
console.log(validate(2)); // false
console.log(validate(4)); // false

.addSchema(Array<Object>|Object schema [, String key])

compileaddSchema非常像,其實我也分不太出來為啥要分兩個,

addSchema vs schemas

直接傳schema

var validate = jv.addSchema(schema)

傳schema並給予一個key值

var valid = ajv.addSchema(schema, 'mySchema').validate('mySchema', data);

除了addSchema的話,也可以在宣告Ajv時直接assignschemas

var ajv = new Ajv({schemas: [schema1, schema2]});

schemas: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them


.addMetaSchema(Array<Object>|Object schema [, String key])

  • 這裡有官方對於Meta Schema的介紹,meta-schema是就是JSON Schema的範本,目前最新的meta-schema是draft-07。

至於為何要addMetaSchema我還不是非常清楚


.getSchema(String key)

只透過uri來取得Schema

ajv.getSchema('http://example.com/schemas/schema.json')

Jquery-Validation - 直得參考的的套件

我認為JSON-validator必須要有良好的message回傳機制才實用,所以我找到了一個我個人認為設計蠻好的套件jquery-validation,因為是jquery所以不太適合套用到reactjs專案,但是可以試著引用它的設計模式來打造一個適合自己專案的validator。可以看看Youtube上的jQuery Validation Plugin 播放清單來了解如何使用。

基本上它就是將驗證的rule與要回傳的message分成兩個property互相對應

這樣遊戲打起來才會爽 - Game Feel 遊戲感

Game Feel 遊戲感

遊戲感 Game Feel 有時也被稱為 Game Juice,指在玩遊戲時經歷的無形感受,最早在2008年在Game Feel: A Game Designer's Guide to Virtual Sensation這本書中提出。

遊戲感比較常用在動作遊戲、射擊遊戲上,一款最基本能動的遊戲,可以不需要任何特效與音效就能夠遊玩,但這樣的體驗非常的差,而我們可以一步步地添加細節,讓遊戲越來越精緻,回饋越來越好。

大部分的Game Feel都不會影響到遊戲最本質的玩法,算是幫遊戲穿上一層華麗的外衣,其中的感覺也很像繪畫時,一個圖層一個塗層的替畫作加上細節的感覺。


Game Feel 影片 - 重點整理

我是看了以下這麼多影片,才整理出這些概念的,感謝這些影片讓我長知識,我得說關於Game Design相關知識,國外的影片與文章比較多,台灣相對來說少,所以得多爬爬國外資源。

雖然上面已經整理出一些項目,但為了讓這些原始資料更原汁原味的呈現,以下是我把看過的影片分享給大家,大家有時間也可以每部都看過。


【專欄】別再揮空氣了!關於遊戲中的「打擊感」三兩事

關於遊戲中打敵人打起來爽不爽,在【專欄】別再揮空氣了!關於遊戲中的「打擊感」三兩事有完整的解說,我覺得這篇太神到我不需任何註解,請大家直接看。

動作遊戲打擊感的來源是什麼?
https://www.zhihu.com/question/21342866


Game Feel: Why Your Death Animation Sucks

講者說:「好的Game Feel,沒人會注意到,但壞的Game Feel,大家會感受的到」

他用Demo,一步一步加上細節:

  • 加入動作的影格動畫 - 就是移動時要有走路動畫,這沒什麼好說

  • 加上音效 - 這邊比較特別的是音效會依據他出現的頻率去調聲音大小,像是頻繁的走路聲,音量就不要太大,然後同樣一個事件,每次可以播放有點不同的音效,這樣不會顯得太死板

  • Death Point - 標出玩家死亡的地方,可以用噴血噴漆等方式,可以讓玩家有成就感

  • 死亡的特效 - 在死亡時加上ScreenShake、chromatic aberration(色差效果)、shockwave(波動)、真實的手把震動

  • 其他 - 路邊的小石頭在你走路時會被你撞開、你的角色眼睛會一直看著敵人、背景會跟著音樂節拍震動、地上塵土在你走路或落地時會飄揚

塵土示意圖

關於音效:之前我朋友聽過思維工坊遊戲公司的音樂總監說:「有音效的作品才是從0分開始往上加,沒有音效的作品還不到0分」,所以音效是很基本該要有的元素


Juice it or lose it - a talk by Martin Jonasson & Petri Purho

對最經典的Brick Pong遊戲加上一大堆細節,甚至開放原始碼讓大家回去嘗試,簡單明瞭。


The Art of Screenshake - 震動遊戲畫面的藝術

Vlambeer公司開發者之一Jan Willem Nijman的演講,他們公司做出的作品節奏跟回饋感都很強,這場講座用Screenshake當作標題,而Screenshake也是加強Game Feel的其中一個方法,他在這場演講逐步加入每個效果並且Demo,是個非場精彩的演講。

以下我整理了,他提到的所有Game Feel要素,他以射擊遊戲為例

  • 加入動作的影格動畫 - 就是移動時要有走路動畫,這沒什麼好說
  • 子彈加速 - 子彈可以快一點,這樣玩起來更有趣
  • 大顆一點的子彈 - 因為這不是真實世界,子彈大一點也無妨,而且看起來的效果也比較好
  • 開槍時的火花 - 在子彈射出時,子彈播放的第一個影格可以是個大白點,像是槍的火花,這樣你每發子彈都會帶火光,更有感覺
  • 不準確的子彈路徑 - 每一發子彈都不會是最直的,可能會有一點點歪
  • 射擊到物品時的效果 - 子彈打到敵人時會有特效,甚至子彈打到牆壁時也有特效
  • 敵人被擊中的特效 - 可能是閃白光、噴血或是受傷動畫
  • 敵人的knockback - 敵人被打到會往後退
  • 永久性permanence - 敵人死後屍體會"永遠"留在場上,可以讓你有體驗戰後成果的成就感,當你回頭到前一個戰場時,也會知道你這邊已經玩過了,進一步的,你還能留下你開槍時掉在地上的彈殼,講者說反正現在電腦很強,這樣電腦還跑得動的
  • camera lerp:camera - 跟著主角時會微微的delay,看起來更滑順
  • camera position - 如果是射擊遊戲,玩家不應該在中間,應該要讓射擊方向的畫面空出更多,這樣才知道要打什麼鬼
  • sleep - 畫面靜止,應該也算慢動作,作者在敵人被打到跟死亡的時候加入非常短的sleep,但他說蠻有效的
  • gun delay, gun kick - 這邊我聽不太懂
  • Meaning - 在死亡時,加入慢動作,死得更磅礡,但我不太知道怎麼定義Meaning在這邊指的是什麼

Secrets of Game Feel and Juice | Game Maker's Toolkit

Game Maker’s Toolkit是個非常棒的頻道,講解蠻多知識的,我也是看這部才開始知道Game Feel的。以下是影片中提到的Game Feel效果。

  • 畫面震動
  • 慢動作(致命一擊的時候)
  • 落地時,地上塵土揚起
  • 開槍後座力
  • 敵人被擊中時會:
    • 閃白
    • 往後退
    • 切換各種被打的動畫圖
    • 噴血
    • 音效

如果你做的是射擊遊戲,可以加上後座力、開槍時就ScreenShake、增快開槍速度。

如果你做平台遊戲,可以加上摩擦力,讓玩家可以在牆上緩慢滑下,不要隨便亂動攝影機,讓玩家玩得不順。


Game Feel - Measuring the Influence of Acceleration and Deceleration - Medialogy, Aalborg University

這我沒全部看完,不過上方影片我幫大家調到講解Juicness的片段,跟其他影片差不多


心得

本來想要自己生一段總整理,把每個要素分門別類,生出一些基本SOP讓人可以去檢驗自己專案的Game Feel,但還是太難了,這邊我只能整理出一個關於特效的心得。

哪裡可以加特效

在遊戲中加入大大小小的特效可以幫助遊戲更豐富,但啥時要加入特效,我認為物體與物體間的互動,都能加上效果,以更宏觀的角度來看,只要物體間有互動,都能加上效果,像是

  • 按下了按鈕,按鈕噴出粒子特效,轉了一圈,播放音效
  • 吃到金幣,金幣縮小不見,噴出金色小星星,然後小星星慢慢往上飄,越來越透明,最後消失
  • 打到怪物,畫面震動了一下,怪物閃白色,上面跳出這一擊對他造成多少數字的傷害

總之,遊戲是沒辦法被所有SOP拆解的,就是有人可以想出體制外的玩法或故事才有趣,以上就是我的Game Feel分享。


Ptt GameDesign 板友對Game Feel的回饋

jerryklu:

其實你也可以看成一個SOP,就是去模擬甚至過度強調所有動作在真實物理上的反應,有些需要強化是因為玩家不能感受到加速度的變化,所以要靠影像甚至是聲音過度強化的方式補足玩家感受不到的,但同樣是加速度,GTS跟蜘蛛人不可能有相同的強化方式,所以遊戲整體的風格、調性很重要。

cjcat2266:

我其中一個SOP是凡有動的東西,就用數值彈簧或easing,然後消除所有非刻意的視覺不連續性(popping)

媽! 我學會Design Pattern了 - 23種Design Pattern範例

https://codepen.io/collection/nYebQp

上方連結收錄了基本的23個Design Pattern,因為Design Pattern並不好讀,加上要原封不動地置入自己的專案中有點難,所以我覺得學習其中的設計精神與實際應用案例應該就夠用了,但是Design Pattern真的太難讀啦!所以我整理了一些較簡單的Javascript Sample Code跟說明在這邊,希望可以幫助到想學Design Pattern的人。

總體來說設計模式分為3大類:

創建型模式,共5種:

工廠方法模式(Factory Method),抽象工廠模式(Abstract Factory),單例模式(Singleton),建造者模式(Builder),原型模式(Prototype)

結構型模式,共7種:

適配器模式(Adapter),裝飾器模式(Decorator),代理模式(Proxy),外觀模式(Facade),橋接模式(Bridge),組合模式(Composite),享元模式(Flyweight)

行為型模式,共11種:

策略模式(Strategy),模板方法模式(Template Method),觀察者模式(Observer),迭代子模式(Iterator),責任鏈模式(Chain of Responsibility),命令模式(Command),備忘錄模式(Memento),狀態模式(State),訪問者模式(Visitor),中介者模式(Mediator),解釋器模式(Interpreter)。

==================================================================

參考資料:

UI/UX 設計指南分享

UI/UX 設計指南分享

身為前端工程師,略懂UI設計好像也是挺重要的,在這邊整理一些網路上整理下來的UI設計的一些小知識,讓大家在設計介面與流程時可以參考。

關於UI Element的使用建議參考看看即可,因為UI的世界瞬息萬變,大家使用的方法也不盡相同,不過我覺得比較重要,也希望大家可以放在心上的概念是Microcopy,我認為這是不管在什麼地方都受用的觀念。

文件大綱:


Dropdowns(下拉式選單)

  • 下拉選單使用時機是選項介於 7 ~ 15 項
  • 少於 7 項的話,建議使用Radio button,可以直接看到所有的選項
  • 超過 15 個選項的話,建議搭配文字搜尋欄位,如下圖

  • 但是當選項中有預設選項,像是字體大小選擇一般“適中”,使用Dropdowns即可
    • 列出所有選項會造成使用者分心
    • 並不鼓勵使用者去更改預設選項.

參考資料:

In case you have less than 7 options you should consider using radio buttons. Your users will be able immediately scan how many options they have and what each of those options are, without clicking (or typing) anything to reveal this information.

Use Radio Buttons Rather Than Drop-downs
If possible, use radio buttons rather than drop-down menus. Radio buttons have lower cognitive load because they make all options visible so that users can easily compare them.


Switch/Toggle(開關按鈕)

單個Switch與CheckBox看起來功用一模一樣,但有一個地方不同,先看看下圖。

當Switch Button按下時同時也會馬上儲存現在的設定,但如果需要按下儲存確認鍵才會更改設定,就要使用Checkbox了。

參考資料:


Placeholder

很多人常常拿Placeholder主要是在輸入欄位中提示「輸入範例」與「輸入限制」,有些人不太喜歡這種方式,在這邊提出另一種對Placeholder的看法

Rather than risk having users stumble while filling out forms or waste valuable time figuring out how they work, the best solution is to have clear, visible labels that are placed outside empty form fields.

Placeholder as an example ↓

Placeholder as help text ↓

Placeholder as secondary label(s) ↓

補充的文字直接放在placholder內的話,使用者在開始打字的時候如果忘記補充文字的內容,必須刪掉輸入內容後才能看到,考驗使用者的短期記憶力,所以像上面提到的,把說明文字放在欄位旁邊是個不錯的做法。

不過placeholder並沒有絕對的限制,如果整體使用情境不錯,還是可以打破上述規則,像是登入頁面的帳號密碼,因為大眾早已習慣兩個輸入框,上方是帳號,下方是密碼,所以就算直接把「帳號」、「密碼」寫在placeholder裡應該也不影響使用。

Nielsen Norman Group曾做過的一項眼球追蹤研究就說明,空的輸入框比有提示文字的輸入框更能引起用戶的關注,而且與空的輸入框相比,用戶更容易忽略有提示文字的輸入框,因為用戶乍看會覺得像已經填寫過的樣子。換句話說,被認為是有用的表單提示文字其實弊大於利,尤其是對習慣大致瀏覽的用戶,但我在瀏覽其他網站的時候,只要placeholder的顏色夠淡,感覺不太會有不順暢的感覺。

現在的Material Design的Adaptive Placeholders可以讓placeholder在輸入時轉換成標題,不過那沒有達到補充說明欄位的效果,所以在此不討論。

Adaptive Placeholders ↓

參考資料:


Microcopy(小提示)

Microcopy是幾句簡短的文字,幫助使用者了解該做什麼或是舒緩對系統感到的困惑,達到的功效如下。以下依照功用分類進行介紹。

1. 消弭使用者的擔憂

對使用者進行承諾,讓使用者安心。

承諾使用者不會濫用他的資料去twitter自動發文,或是對他的朋友發送垃圾信以及和自動關注帳戶 ↓

告知使用者取得他們資料的原因 ↓

2. 提供訊息補充說明

提到這個就必須説說Information Icon了,因為有些資料輸入欄位很難從欄位名稱看出該輸入的什麼樣的資訊,所以通常會在輸入欄位的Label旁加上information icon,icon上會有是 ? 或 i 的樣式。按下icon的時候會跳出tooltip,詳細解說這欄位的用意或該輸入什麼。

Information Icon 的用處

Contrary to help hints, tooltips at first hide the information and then display it on demand. They are usually fuelled by an icon with a question mark. Help information is given by keeping on help icon or selecting it. Once the mouse goes far from the icon, the tooltips vanishes. These can effectively help in reducing clutter especially when the help text is lengthy.

Facebook 的 info icon ↓

Walmart 的 info icon ↓

Line Creator 的 info icon ↓

如果這個資訊相當重要,那就可以直接顯示文字資訊,而不要用info icon

If the information is extremely salient, consider displaying it at all times rather than as a tooltip.

參考資料:

3. 有用的錯誤訊息

當事情出錯時,知道發生什麼狀況有益解決它。如果你對錯誤的說明很模糊,想解決這些錯誤的使用者會很痛苦,如錯誤訊息的範例下圖。

  1. Error message should be clear

錯誤訊息不要只寫「發生錯誤,請再試一次」這樣含糊的字句,你要想像你正在跟使用者說話,使用更好懂更具體的描述吧!

  1. Error message should be helpful

除了給予錯誤原因,最好給予解決方法,在下圖中,當登入失敗時,系統不只告知錯誤狀況,還提供了2個解決方法,後者的方法還提供了連結幫助使用者解決問題。

  1. Error message should be specific to the situation

大部分網站對所有驗證狀態只會給給予一種錯誤訊息,然而MailChimp官方網站中填寫email欄位時會有3種不同的錯誤訊息,這種客製化的錯誤訊息我覺得最完美,但是也非常費工,我覺得在使用者最容易犯錯的欄位進行這種優化即可。

以上提到撰寫錯誤訊息的建議是從How to Write a Perfect Error Message這篇文章取出最重要的三點,其他說明可以點進去看看。

參考資料:

What is Microcopy: The small bits of text/copy that help instruct and alleviate(減緩) the concerns of your users.

By definition, the microcopy of a website consists of small chunks of text used in different elements – labels, buttons, error messages, tooltips, etc.


UI 頁面狀態:空白狀態(empty state)

當你第一次使用系統時,有很多頁面都沒使用過,例如使用相片編輯軟體,但是沒建立任何檔案,此時畫面會是空白的一片,這就是所謂的空白狀態(empty state),請看 設計師最常忽略的 UI 頁面狀態:空白狀態(empty state),將會告訴你如何將空白狀態(empty state)處理的更完善。


Label Placement

Label與Field的關係也是一門學問,擺放的位置主要分成以下三種

Compare Label Placement

Label 在 Field 正上方

普遍的狀況下最實用的一種排版方式,它方便觀看,如果把Label放在Field左側,眼睛注視表單時就必須在Label與Field之間左右來回移動,增加負荷,另一點是表單如果支援多國語系,label長度會不同,尤其法文的長度大致是英文的兩倍長。

優點

  • Label與Field靠得很近,方便觀看,降低負荷
  • Label在多國語系下長度不同,也不會影響排版
  • 在行動裝置上更方便觀看,如果Label跟Field分成左右兩邊的話,畫面會很擠

缺點

  • 表單會變的很長,而且無法一次觀看到所有的Label。

Label 在 Field 左側

優點

  • 整張表單的高度可以縮減,Form不會太長
  • 可以一次觀看到所有的Label,快速跳到你想填的欄位

缺點

  • 在填表時,眼睛必須左右來回觀看Label與Field,增加負荷
  • Label在多國語系下長度不同,容易影響到排版
  • 在行動裝置上排版會很擠,不方便觀看

Label 在 Field 左側 - 靠右對齊的 Label

如果要把Label放在Field左側,至少用向右對齊的版本

Label 在 Field 左側 - 靠左對齊的 Label

效果最差,眼睛要左看右看,負荷很大,應該算是最要不得的排版,雖然有人說在掃描整個Label時,因為靠左對齊的關係,比較好看,但整體起來還是不好用的。

結論

在一般使用者填表的時候,我覺得盡量都用「Label在Field正上方」,但是在管理或是設定頁面時,有時候欄位很多(像是管理介面),想設定的欄位只有特定一兩個,並不需要從頭到尾每個欄位都填過,或許用「Label在Field左側 - 靠右對齊的Label」也是一種很好的選擇。若想使用「Label 在 Field 左側 - 靠左對齊的 Label」的方法,用有框線的表格更能幫助閱讀。

參考資料


Form

按鈕命名

避免使用"送出"之類的通用詞語來按鈕表示行為,因為它們給人的印象太通用了。相反地,請說明按鈕在點擊時執行的行為,例如"創立我的帳戶"或"訂閱每週優惠"。


按鈕位置

按鈕位置雖然查到了一些研究與解釋,但似乎沒有個定案,以下大家參考看看就好。

在Dialog中,按鈕會被擺在右下角,因為放在右邊的東西有下一步的感覺,也符合閱讀的順序,而如果有Cancel與OK的多個按鈕,主按鈕應該放在最右側,也就是OK按鈕。

↓ 符合閱讀順序

↓ User會看完所有按鈕再決定按哪個,所以主按鈕在左側會讓眼神多飄移一次

但是在整頁的大型的表單中(非Dialog),lukew的文章中建議把按鈕放左側是較好的方案,我覺得按鈕對齊按鈕input欄位應該是關鍵,因為user可能會回頭查看表單input的資料,再往下找按鈕的時候,也能較快速定位到按鈕。所以放左邊跟中間的按鈕較多,但在表單中把按鈕放右邊比較少。

↓ Airbnb

↓ Github

參考資料


別太輕易送出表單

在登入頁面的帳密輸入框與搜尋引擎的搜尋Bar中常會實作按下Enter鍵就送出表單的功能,其他地方可以用這招嗎?我個人的看法是如果這方法不會改變資料的話就無所謂,但如果是新建或是刪除資料的表單最好還是不用比較好,雖然FB的發文就是按下Enter就送出,所以這方便大家就參考看看吧!


表單驗證時機

驗證分成兩類,而inline validation又再分兩類

  • After Submit Validation
  • Inline Validation
    • Validate during the data entry
    • Validate after the data entry

After Submit Validation

有一派說法是贊成After Submit Validation,他們認為在user輸入資料時一直驗證會打斷user思考,user在填飽的時候會進入「填表模式」,當送出資料後,會轉換成「修改模式」去修正剛剛沒打對的地方,所以他們認為不應該打斷填表模式。

Inline Validation:

在使用者輸入某個欄位時,就進行驗證,不等到最後送出後才驗證,首先先來看兩種inline validation的驗證方式。

  • Validate during the data entry:
    輸入資料的同時就會一邊即時驗證。

  • Validate after the data entry:
    在輸入完資料後,離開這個輸入欄位或是當已經輸入到此欄位長度的最大值,才開始驗證。


在「Usability Testing of Inline Form Validation」文中提到如何做出最好的inline validation,我把結論整理在下方,

常見錯誤1: 倉促的驗證

如果直接使用「Validate during the data entry」,使用者在點擊欄位或是才剛輸入第一個字元時就會看到錯誤訊息,user會覺得自己明明還沒打完,為什麼要被這樣糾正,體驗並不好。

解法:在第一次輸入的時候,記得使用「Validate after the data entry」,在使用者確實輸入完資料再驗證,體驗會較佳,如此便能解決常見錯誤1。

常見錯誤2: 過時的驗證

在「Validate after the data entry」後發現資料驗證錯誤,顯示錯誤訊息,而user回頭進行修改的時候,修改完後離開欄位(onblur event),系統會再度驗證,但是當我們修改資料時,可能已經通過驗證了,在等到離開欄位後再認證顯得不夠即時,這也就是常見錯誤2。

解法:修改錯誤資料時,使用「Validate during the data entry」,每輸入一個字元就驗證一次,一發現通過驗證就馬上隱藏錯誤訊息,如此一來就能達到良好的驗證效果。

例外:

某些狀況下,可以在資料尚未輸入完之前就驗證並給予警吿

  • 在數字欄位中輸入英文字母
  • 在複雜的驗證機制輸入不該出現的字元,像是不符合規範的信用卡號
  • 太長的輸入(例如15位電話號碼)

良好範例

參考資料


以下是留著之後消化的資料,但應該會消化不良

用jest+enzyme來寫Reactjs的單元測試吧!

單元測試

這份文件將會告訴你哪些資訊

這份文件會告訴你如何使用jest與enzyme兩個工具來撰寫單元測試,並應用在你的react專案裡,你將會學會以下所提到的項目。

有關單元測試的前言

前端的單元測試在很多人看來都是一個可有可無的東西,理由一般有下面幾條(以下內容統一稱單元測試為單測):

  • 寫單測比較費時,有這個時間不如多做幾個需求
  • 測試在驗收的時候對頁面的功能都會操作一遍,寫單測相當於做無用功
  • 後端提供給前端的接口需要保證質量,因此需要做單測,但前端很少需要提供接口給其他人

其實,我大體上是同意以上觀點的。在大部分的情況下,如果公司的業務不復雜,是完全沒必要做單測的。但如果涉及到以下幾個方面,你就要考慮是否有必要引入單測了:

  • 業務比較複雜,前端參與的人員超過3人
  • 公司非常注重代碼質量,想盡一切辦法杜絕線上出bug
  • 你是跨項目組件的提供方
  • 你在做一個開源項目

參考資料: React單元測試:Jest + Enzyme(一)

單元測試 - 3A原則

在撰寫單元測試的程式碼時,有個 3A 原則,來輔助設計測試程式,可以讓測試程式更好懂。3A 原則如下:

  1. Arrange : 初始化目標物件、相依物件、方法參數、預期結果,或是預期與相依物件的互動方式。
  2. Act : 呼叫目標物件的方法。
  3. Assert : 驗證是否符合預期

Jest - Test runner, JavaScript testing framework

Jest是一個JavaScript的測試框架,也是所謂的Test runner,類似的項目大概有Jasmine(茉莉花), Mocha(摩卡咖啡), AVA這幾種測試框架,Mocha應該是最多人使用的,但後來Facebook延續Jasmine開發Jest,也是目前專案預設的測試框架,所以就繼續沿用。

因為Jest是新框架,所以也比Mocha多了一些新功能,或是讓語法更加精鍊。

主要API使用教學

  • jest指令是使用jest-cli模組,在react-boilerplate中已經安裝在devDependencies內,相關設定也幫你設定好了。
  • 可以安裝vsCode的jest-snippets套件

先寫一個簡單的function來測試,接著用pass.test.js檔案來寫測試程式,完成後在cmd下執行jest,jest就會去所有目錄中找檔名是.test.js.spec.js結尾的檔案來跑測試程式,把測試程式都放在根目錄中的__test__資料夾也可以。

不過在我們專案直接下npm run test就可以了,jest指令已經被整合進去了。

這是我們要測試的pass.js

const isPass = score => {
  if(score>=60){
    return true;
  }else{
    return false;
  }
};
module.exports = isPass;

這是我們的測試程式pass.test.js

const isPass = require("./pass");

//describe中通常寫一個元件或是一個function
describe("function isPass()", () => {
  
  //it是這個元件或function中的test case
  it("should return true when score is 60", () => {
    expect(isPass(60)).toBe(true); // 期待isPass(60)回傳的結果是true
  });
                                     
  it("should return true when score is 45", () => {
    expect(isPass(45)).toBe(false);
  });
                                     
});
  • describe: 將相關的測試案例整合起來定義一個測試結果(test suite),可以使用beforeEach,afterEach決定再跑測試之前或之後要先執行的區塊。Test Suites的數字就是describe的數量。
  • it , test: 定義一個最小的測試案例(test case)。Tests的數字就是describe的數量。it是test的alias,所以兩個是一樣的東西
  • expect: 用來判斷是否和預期值相同的斷言庫。
  • toBe: 比較兩物件是否有相同的值,常用來比較數值。

參考資料:React 前端單元測試教學

jest 的一些斷言方法

// be and equal
expect(4 * 2).toBe(8);                      // ===
expect({bar: 'bar'}).toEqual({bar: 'baz'}); // == deep equal
expect(1).not.toBe(2);

// boolean
expect(1 === 2).toBeFalsy();
expect(false).not.toBeTruthy();

// comapre
expect(8).toBeGreaterThan(7);
expect(7).toBeGreaterThanOrEqual(7);
expect(6).toBeLessThan(7);
expect(6).toBeLessThanOrEqual(6);

// Promise
expect(Promise.resolve('problem')).resolves.toBe('problem');
expect(Promise.reject('assign')).rejects.toBe('assign');

// contain
expect(['apple', 'banana']).toContain('banana');
expect([{name: 'Homer'}]).toContainEqual({name: 'Homer'});

// match
expect('NBA').toMatch(/^NB/);
expect({name: 'Homer', age: 45}).toMatchObject({name: 'Homer'});

Jest 覆蓋率解說

stackover的網友講解如何看覆蓋率報表

  • Stmts(Statement):

    • 有多少比例的statment被執行到,一個console.log("statement_01");及算是一個statment,然而一行line中可以有多個statement,如下面choice12.js程式中的第8行,一行line中就包含console.log("statement_01"); console.log("statement_02");
      共2個statement。
  • Branch:

    • 我們可以看到function choice12中的switch有3種可能性,但我只測試了其中兩種,Default的情境並沒測試到,所以3個branch只測試到其中2種,百分比就是66.67%。
  • Funcs(Functions):

    • 一個檔案中有多少比例的Function被執行,choice12.js中只有一個Function,所以如果去測試那個function,覆蓋率就會100%。
  • Lines:

    • 如上面statement所說,基本上lines應該是大於等於statement的。
  • Uncoverd Lines:

    • choice12.js程式中的8,9行沒被測試到

想要讓覆蓋率變成100%,只要將測試程式中的10,11,12行註解拿掉即可。


使用jest查看覆蓋率

在react-boilerplate中,使用npm test可以直接查看覆蓋率,一般情況則使用jest --coverage查看,檢查的範圍可以在package.json設定,如下

"jest": {
 "collectCoverageFrom": [
 "app/**/*.{js,jsx}",
 "!app/**/*.test.{js,jsx}",
 "!app/*/RbGenerated*/*.{js,jsx}",
 "!app/app.js",
 "!app/global-styles.js",
 "!app/*/*/Loadable.{js,jsx}"
]

如何只對單一檔案查看覆蓋率

因為無法在react-boilerplate中似乎無法對「單一」測試檔進行jest --coverage,安裝jest-single-file-coverage方能使用。

安裝 jest-single-file-coverage

在專案中的package.json檔案中的script屬性加上

"test:single": "node ./node_modules/jest-single-file-coverage"

就能使用npm run test:single <file_path>對該路徑下的測試檔案進行測試,並顯示coverage


choice12.js

const   choice12   =   choice   => {
  switch (choice) {
    case 1: 
      return 1;
    case 2:
      return 2;
    default:
      console.log("statement_01"); console.log("statement_02"); //沒被測試到
      return 0; //沒被測試到
  }
};

module.exports = choice12;

choice12.test.js or chocie12.spec.js

const choice12 = require('./choice12');

describe('function choice12()', () => {
  it('should return 1 when enter 1', () => {
    expect(choice12(1)).toBe(1);
  });
  it('should return 2 when enter 2', () => {
    expect(choice12(2)).toBe(2);
  });
  // it('should return 0 when enter 5', () => {
  //   expect(choice123(5)).toBe(0);
  // });
});

將覆蓋率匯出成HTML報表觀看

在command line下的Uncoverd lines大概只能顯示... 19,20,22,23短短幾行,非常不方便,所以這邊分享觀看jest匯出的精美網頁版coverage報表。

在jest中測試覆蓋率後,似乎預設會匯出HTML檔案報表(不確定是不是react-boilerplate中
已經設定好的關係),在每次測試覆蓋率後,可以在根目錄看到coverage資料夾,這個資料夾也在.gitingore中被記載,直接開啟coverage/lcov-report/index.html就能看到精美的覆蓋率報告。

在index.html中可以看到每個檔案的覆蓋率

點擊index.html中的檔名則可觀看每個檔案中覆蓋率

觀看程式碼時,不同顏色與符號分別代表哪些資訊:

  • 粉紅色的程式碼: 尚未被執行的statement或function
  • 黃色的程式碼: 沒被涵蓋到的branch
  • E stands for 'else path not taken', which means that for the marked if/else statement, the 'if' path has been tested but not the 'else'.
  • I stands for 'if path not taken', which is the opposite case: the 'if' hasn't been tested.
  • The Nx in left column is the amount of times that line has been executed.

↓「I代表的是if-else的if沒被執行,E代表else的部分沒被執行」示意圖

以上資訊是參考類似的測試工具 Istanbul - a JavaScript test coverage tool


Snapshot功能

Snapshot testing is another new idea from Facebook. It provides an alternate way to write tests without any assertions. To write tests using assertions, Enzyme is quite useful.

快照(Snapshot)可以測試到組件的渲染結果是否符合預期,預期就是指你上一次錄入保存的結果,toMatchSnapshot方法會去幫你對比這次將要生成的結構與上次的區別。使用snapshot,可以防止無意間修改组件的某些部分,以及快速增加測試程式的覆蓋率,剩下像是click事件這種不太能直接用snapshot比對的部分,再靠手動去撰寫程式檢測。

在這邊會使用enzyme,可以先到下一章了解enzyme

import React, { Component } from 'react';
import { render } from 'enzyme';
 
it('renders correctly', () => {
  const wrapper = render(
    <div id="helloworld">
      <strong>Hello World!</strong>
    </div>
  );
                               
  expect(wrapper).toMatchSnapshot();
});

第一次執行測試的時候,只要遇到toMatchSnapshot,就會在測試程式的同一層建立一個__snapshots__資料夾,並且產生snap檔,如果原來的測試檔案叫做index.test.js,那麼所產生的snap檔為index.test.js.snap

index.test.js.snap快照檔內容

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<div
  id="helloworld"
>
  <strong>
    Hello World!
  </strong>
</div>
`;

第二次跑測試程式的時候,就會去比對snapshot,如果比對結果有出入,就會跳出失敗訊息,當你想要更新snapshot時,只要執行jest -ujest --updateSnapshot 就可以了。u是update的意思。

特別要注意的是,如果你Component內容改錯了,還下jest -u的話,就會把錯誤的DOM給記錄到snapshot裡面,所以在update之前記得要確認自己現在的程式OK才update。

如過只要測試單一的test.js檔案,像是我如果想執行/app/components/choice資料夾底下的測試程式,就在指令的地方下jest /app/components/choice,就會只測試對應到的檔案,在cmd上會顯示Ran all test suites matching "/app/components/choice".,同樣的,如果只要更新相關檔案的snap檔,就下jest /app/components/choice -u就可以了。

註解: 個人覺得snapshot只能拿來當輔助,畢竟它只能比對現在DOM架構之前有無差異,但並不知道什麼才是"真正對的"架構。


Mock(模擬) Function

以上的斷言基本是用在測試同步函數的返回值,如果所測試的函數存在異步邏輯。那麼在測試時就應該利用jest 的mock function 來進行測試。通過mock function 可以輕鬆地得到回調函數的調用次數、參數等調用信息,而不需要編寫額外的代碼去獲取相關數據,讓測試用例變得更可讀。

// 輸入一個數字,它會回你平方跟乘以3的數字
function getDoubleAndMultiplyby3(val, callback) {
  if (val < 0) {
    return;
  }
  callback(val * val, val * 3);
}

const mockFn = jest.fn();
getDoubleAndMultiplyby3(5, mockFn);
getDoubleAndMultiplyby3(10, mockFn);

describe("testing getDouble() with a mock function", () => {
  it("it should was called least once", () => {
    // expect(mockFn).toHaveBeenCalled();
    expect(mockFn).toBeCalled();
  });
  it("it should was called twice", () => {
    expect(mockFn).toHaveBeenCalledTimes(2);
  });
  it("it should was return 25,15 in first callback", () => {
    // expect(mockFn).toHaveBeenCalledWith(25,15);
    expect(mockFn).toHaveBeenCalledWith(25, 15);
  });
  it("it should was return 100,30 in last callback", () => {
    // expect(mockFn).toHaveBeenLastCalledWith(100,30);
    expect(mockFn).lastCalledWith(100, 30);
  });
});

Enzyme - React版的jquerycheerio

enzyme可以把它想像成一個react版本的jquery,使用enzyme中的shallow,就可以將react component轉換成像jquery一樣的物件,接著可以使用像是find之類的method取得所要的資訊進行比對驗證.

除了shallow以外,還有mount與render兩種method,但基本的shallow最常用。

  • mount:Full Rendering,非常適用於存在於DOM API存在交互組件,或者需要測試組件完整的生命週期
  • render:Static Rendering,用於將React組件渲染成靜態的HTML並分析生成的HTML結構。render返回的wrapper與其他兩個API類似。不同的是render使用了第三方HTML解析器和Cheerio

  • Airbnb 開發的開放原始碼專案。
  • 供測試使用的 utility,非框架。所以不包含測試環境, 斷言庫。
  • 引身自 TestUtils, JSDOM, CheerIO。
  • 提供可以渲染出 react 元件,並可模擬(simulate)使用者行為,如input change, button clicked。
  • 核心有包含jquery,可以使用選擇器搜尋DOM的樹結構。

簡易的Enzyme範例

import { shallow } from 'enzyme';

import MyComponent from './MyComponent';
import Foo from './Foo';

describe('<MyComponent />', () => {
  
  // 檢查是否成功渲染一個MyComponent
  it('renders <MyComponent /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper).toHaveLength(1);
    // expect(wrapper).exists()); //應該與上一行效果一樣
  });
                                   
  // 檢查MyComponent中是否成功渲染3個Foo                
  it('renders three <Foo /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find(Foo)).toHaveLength(3);
  });
                                   
  // 檢查MyComponent中是否成功渲染3個Foo                
  it('counter+1 when the last button is clicked', () => {
    const wrapper = shallow(<MyComponent />);
    //找出wrapper中的最後一個button,並且用滑鼠點擊
    wrapper.find('button').last().simulate('click');
    // ... expect(counter).toBe(1);
  });
                                   
});

如何測試有delay的Component

兩個重點:

  • 使用async()
  • 使用await sleep(毫秒)

以下的測試的情境是當error發生的時候,畫面下方的Snackbar會跳出並顯示錯誤訊息,2秒後自動消失。
open這屬性會從原本的false變成true,並在2秒後變回false,等待2秒這件事情我們使用await sleep(2050)來實作,2050是因為2000+50,如果剛好用2000的話怕會有誤差,所以多50毫秒。另外,如果程式中使用到await,則必須把程式包在async()內。

it('set props to error, Snackbar should be opened', async () => {
  const { wrapper, dispatch } = setup();
  expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();
  wrapper.setProps({ error: 'RequestToken.Invalid' });
  expect(wrapper.find(Snackbar).prop('open')).toBeTruthy();
  await sleep(2050); // wait for more than 2000 ms(autoHideDuration)
  expect(dispatch).toHaveBeenCalledWith({ error: '', type: 'app/Main/REQUEST_ERROR' });
  wrapper.setProps({ error: '' });
  expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();
});

究竟是該測些什麼

究竟Unit Test該測哪些項目呢? 以下是一些網路文章給的想法

React Component Testing with Enzyme 文章的結論

作者整理出3個他覺得該測試的項目

  • 本身性質測試:
    • 這Component本身的是哪種類型的DOM,是div還是input或其他
    • 這Component的Class有哪些
    • 這Component在帶入不同props下是否有切換到正確的Class
  • 包含測試:
    • 是否包含正確的子Component,像LoginPage中就會有兩個TextField跟一個RaisedButton
  • Event 測試:
    • 例如 clicking, dragging, keyboard input, etc,本文章範例是按下按鈕後會新增一個Todo,所以會去模擬按按鈕一次後預期Todolist中的item要多一個

The Right Way to Test React Components 文章的結論

  • 每個Component測試都該先注意的是它會render的樣子,至少要測這元件會顯示哪些基本的DOM
  • 另外要測試Component所收到props以及本身就有的state
  • 以及測試這Component會有哪些互動event(例如 clicking, dragging, keyboard input, etc)
  • 不要測試 Prop types,因為這在主程式就已經有判斷機制了,不值得在測試程式再測一次
  • Inline styles CSS通常不值得我們去測試,我覺得可能是因為CSS很常會修改,感覺太細節了
  • 主Component旗下的component有哪些,以及收到props對Componet的影響,這是重要的測試項目

結論By Allen

  • 我覺得本身性質測試不是那麼重要,包含測試比較重要一點
  • Props是關鍵,不同的Props基本上會呈現不同Component,這部分搭配Coverage較容易觀察出哪些可能還沒有檢測到
  • Event一定要測
  • 參考Coverage,盡量達到100%覆蓋率,但是不用走火入魔

Redux github 中的測試程式範例


如何測試Container - Testing Container in Provider

單純的component測試因為沒跟其他物件以及資料有相依的狀況,較容易測試,而Container則相對複雜,我們可以看到下面的<App />被包在很多層DOM裡面,如果缺乏這些外層DOM的資訊,將很難對<App />中的Container進行測試,解決方式就是模擬外層DOM將這些模擬的資訊傳遞給裡面的DOM。

<Provider store={store}>
  <MuiThemeProvider muiTheme={getMuiTheme(customizedTheme)}>
    <LanguageProvider messages={messages}>
      <ConnectedRouter history={history}>
        <App />
      </ConnectedRouter>
    </LanguageProvider>
  </MuiThemeProvider>
</Provider>
  • Provider:提供store
  • MuiThemeProvider:提供Material UI的資訊
  • LanguageProvider:真正的Provider是IntlProvider,提供跨國語言包
  • ConnectedRouter:負責記錄history

解決方法就是建立一個enzymeHelper.js檔案,在裡面實作mountWithProviders取代enzyme原生的mount,在mountWithProviders 執行enzyme的mount,並將所有要傳給Container的資訊放進context內,建立了出來的wrapper就能夠正常運作。

enzymeHelper.js

import React from 'react';
import { shallow, mount } from 'enzyme';
import { IntlProvider, intlShape } from 'react-intl'; // mock IntlProvider in LanguageProvider
import getMuiTheme from 'material-ui/styles/getMuiTheme'; // mock MuiThemeProvider
import ReactRouterEnzymeContext from 'react-router-enzyme-context'; // mock ConnectedRouter
import configureStore from 'redux-mock-store'; // mock Provider

// set up Provider
const store = configureStore([])({});
store.dispatch = jest.fn();
const dispatch = store.dispatch;
// set up intlProvider
const messages = require('translations/en.json');
const intlProvider = new IntlProvider({ locale: 'en', messages }, {});
const { intl } = intlProvider.getChildContext();
// set up MuiThemeProvider
const muiTheme = getMuiTheme();
// set up ConnectedRouter
const router = new ReactRouterEnzymeContext().get().context.router;

// assign props 'dispatch' into node, node is the component which we want to test.
function nodeWithProps(node) {
  return React.cloneElement(node, { dispatch });
}

// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to shallow
export function shallowWithProviders(node) {
  return shallow(nodeWithProps(node), { context: { intl, muiTheme, router, store } });
}

// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to mount
export function mountWithProviders(node) {
  return mount(nodeWithProps(node), {
    context: { intl, muiTheme, router, store },
    childContextTypes: {
      intl: intlShape,
      muiTheme: React.PropTypes.object,
      router: React.PropTypes.object,
      store: React.PropTypes.object,
    },
  });
}

enzymeHelper.js的使用方法

import React from 'react';
import { mountWithProviders } from 'enzymeHelper';

const defaultProps = { 'test': "testVaule" }

const setup = (props = {}) => {
  const wrapper = mountWithProviders(<MyContainer {...defaultProps} {...props} />);
  const actions = {
    testMethod: wrapper.instance().testMethod,
    testOtherMethod: wrapper.instance().testOtherMethod,
    mock: (...methods) => { //you need to implement mock() in every test.js
      methods.forEach((method) => {
        wrapper.instance()[method] = jest.fn();
        actions[method] = wrapper.instance()[method];
      });
    },
  };
  return {
    wrapper,
    actions,
    dispatch: wrapper.props().dispatch,
    // customize common DOM you want to test
    // ex: loginBtn: wrapper.find(FlatButton).at(0),
  }
};

describe('functions of <MyComponent />', () => {
  it('testMethod()', () => {
    const { wrapper, actions, dispatch } = setup();

    // testMethod() include a dispatch
    actions.testMethod();

    // check the testMethod() trigger dispatch or not 
    expect(dispatch).toHaveBeenCalledWith({
      data: { testContent: null, type: 'Text' },
      type: 'app/MyComponent/TEST_METHOD',
  });
      
 it('click button to trigger testMethod()', () => {
    const { wrapper, actions } = setup();

    // mock testMethod()
    actions.mock('testMethod');
    /* 
     *  if you want to mock multiple functions
     *  you can assign mutiple parameters into actions.mock() 
     *  ex: actions.mock('testMethod','testOtherMethod');
     */

    wrapper.find('button').first().simulate('click');

    // testMethod() will be called once after Clicking button
    expect(actions.testMethod).toHaveBeenCalledTimes(1);
});

直接使用下方的Template開始開發Container的單元測試

// *** REMOVE THE COMMENTS IN THIS FILE TO START CODING UNIT TEST ***

/*
 * ComponentName <= replace ComponentName with the Component you want to test.
 * FunctionName <= replace FunctionName with the function you want to test.
 */

import React from 'react';
import { shallow } from 'enzyme';
import { mountWithProviders } from 'enzymeHelper';

/*  TODO: import the component you want like material-ui TextField or FlatButton */

import { ComponentName } from '../index';

const defaultProps = {
  /*  TODO: need to put default props here */
};

const setup = (props = {}) => {
  const wrapper = mountWithProviders(<ComponentName {...defaultProps} {...props} />);
  const actions = {
    /*  TODO: customize common DOM you want to test */
    mock: (...methods) => {
      methods.forEach((method) => {
        wrapper.instance()[method] = jest.fn();
        actions[method] = wrapper.instance()[method];
      });
    },
  };
  return {
    wrapper,
    actions,
    dispatch: wrapper.props().dispatch,
    /*  TODO: customize common DOM you want to test */
  };
};

describe('<ComponentName />', () => {
  it('match snapshot', () => {
    const wrapper = shallow(<ComponentName {...defaultProps} />);
    expect(wrapper).toMatchSnapshot();
  });
  it('must have ......', () => {
    const { wrapper } = setup();
    // TODO: check if the important DOM exists ex: button, input, table
    // ex: expect(wrapper.find(FlatButton)).toHaveLength(1);
  });
  // TODO: check events executing correctly or not, ex: click,change,keypress
  // TODO: check UI changing correctly or not when different props assign
});

describe('functions of <ComponentName />', () => {
  it('FunctionName()', () => {
    const { actions } = setup();
    // if you want to mock FunctionName(), then use actions.mock('FunctionName');
    actions.FunctionName();
    // TODO: check the result correct or not 
  });
  // TODO: test other functions
});

Component的單元測試Template

//直接使用下方的Template開始開發Container的單元測試
import React from 'react';
import { shallow } from 'enzyme';

// TODO: import the component you want like material-ui TextField or FlatButton

const   defaultProps = {
// TODO: need to put default props here
};

import CheckboxOption from '../index';


describe('<CheckboxOption />', () => {
  it('match snapshot and must have ......', () => {
    const wrapper = shallow(<CheckboxOption {...defaultProps} />);
// TODO: check if the important DOM exists ex: button, input, table

 // ex: expect(wrapper.find(FlatButton)).toHaveLength(1);

  });
// TODO: check events executing correctly or not, ex: click,change,keypress

 // TODO: check UI changing correctly or not when different props assign

如何測試Redux Saga

Saga的測試方式跟UI的測試方式有點不同,所以在這邊解說該如何測試,其實主要就是把原本saga.js中所有yield後的put,call等相關指令全部測過一次。

範例程式

api.js

const api = {
  fetchProductAPI() {
    return 'iphone';
  },
};
export default api;

saga.js

import { call, put } from 'redux-saga/effects';
import api from './api';

export default function* fetchProduct() {
  try {
    yield call(api.fetchProductAPI);
    yield put({ type: 'PRODUCTS_RECEIVED', product: 'iphone' });
  } catch (error) {
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error });
  }
}

saga.test.js

/* eslint-disable redux-saga/yield-effects */

import { put, call } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';
import fetchProduct from './saga';
import api from './api';

describe('fetchProduct()', () => {
  // gen = fetchProduct(); <== original style
  const gen = cloneableGenerator(fetchProduct)();

  it('try', () => {
    const clone = gen.clone();
    expect(clone.next().value).toEqual(call(api.fetchProductAPI));
    expect(clone.next().value).toEqual(put({ type: 'PRODUCTS_RECEIVED', product: 'iphone' }));
  });
  it('catch', () => {
    const error = 'product not found';
    const clone = gen.clone();
    clone.next(); // <== before throw error, you need to execute gen.next();
    expect(gen.throw('product not found').value).toEqual(put({ type: 'PRODUCTS_REQUEST_FAILED', error }));
  });
});

Redux-Saga的Library (目前沒使用Library)

Redux-Saga也有Library,其中星星數最多的框架是redux-saga-test-plan (415 star),但是目前一直套用失敗,有時間與需求的話再回頭研究,有個網站分析各個Redux Saga Test Library。

saga測試程式如何傳遞參數

generator.next(value)中的value該填入什麼才對呢?

可以看到下面的saga.test.js的第3行中generator.next(product).value,這個product填這邊的原因要看到saga.js,saga.js中第2行的yield put({ type: 'CALL_API' });是接續第1行的const product = yield call(api);,所以要將product填在next中讓之後的測試程式可以用到product

saga.js程式片段

const product = yield call(api);
yield put({ type: 'CALL_API' });

saga.test.js程式片段

const prouduct = "mock product data";
expect(generator.next().value).toEqual(call(api));
expect(generator.next(product).value).toEqual(put({ type: 'CALL_API' }));

如何解決saga中的if-else分支問題

有時候你的 saga 可能會有不同的結果。為了要測試不同的 branch 而不重複所有流程,你可以使用 cloneableGenerator utility function,他可以複製某一步驟的generator,範例如下

import { cloneableGenerator } from 'redux-saga/utils';

// 原本的程式是 const gen = getOPList(action);
// 如果要複製品這個generator,就改寫成下面這行
const gen = cloneableGenerator()();

it('use clone', () => {
    //將剛剛的gen複製一份成clone
    const clone = gen.clone();
    expect(clone.next().value).toEqual(put({type:"ohya"})));
});

如何測試 try-catch 中的 catch 事件

想了解整段程式怎麼寫的可以參考完整範例程式,其中要跳到catch的case中,就必須使用throw(),但在throw()之前我們必須先執行一次next()才行,error部分的測試程式如下。

it('catch', () => {
  const error = 'product not found';
  const gen = fetchProduct();

  gen.next(); // <== before throw error, you need to execute gen.next();

  expect(gen.throw('product not found').value)
      .toEqual(put({ type: 'PRODUCTS_REQUEST_FAILED', error }));
});

如何測試Saga程式中的flow function

基本上flow function都使用takeEvery,目前測試遇到takeEvery都會失敗,後續再研究如何測試takeEvery,或是根本不需要去測試。

常見錯誤

觸發event的方式會因應你使用shallowmount會有所不同,下面以onChange事件為例寫下兩種不同範例。

shallow

const input = wrapper.find(TextField);
input.simulate('change', { target: { value: 'newValue' } });

mount

const input = wrapper.find(TextField);
input.prop('onChange')({ target: { value: 'newValue' } });

取得Dialog中actions裡面的按鈕

wrapper
  .find(Dialog)
  .prop('actions')
  .forEach((btn) => {
    btn.props.onClick();
  });

取得Dialog內Component的方法:中的內容無法直接使用find來抓取的,所以我們用prop('children')來抓取,但隨著每個Dialog內部有著不同的

const dialog = wrapper.find(Dialog);

dialog.prop('children')[0].props.onChange();
const dialog = wrapper.find(Dialog);

dialog.prop('children').props.children.forEach((el) => {
  if (el.props.children) {
    el.props.children[0].props.leftCheckbox.props.onCheck(null, true);
    el.props.children[0].props.leftCheckbox.props.onCheck(null, false);
    el.props.children[0].props.nestedItems.forEach((listItem) => {
      listItem.props.leftCheckbox.props.onCheck(null, false);
      listItem.props.leftCheckbox.props.onCheck(null, true);
    });
  }
});

參考資料

教學影片:

文件與網站:

研究中的題目

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.