Jennie
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# 手把手一起 TDD ![TDD cycle](https://i.imgur.com/9HCEV2B.png) 想了解更多 TDD,可以參考 [TDD - 測試驅動開發](https://hackmd.io/2QXKK978Sei4LMHQWhN-gQ),此篇是實作篇,手把手帶你體驗 TDD :dash::dash: ## TDD Flow 再看一眼 TDD 流程,稍微有個印象,我們就繼續往下看 ~ ![TDD flow](https://i.imgur.com/obbaftB.png =500x) #### Step 1. 列出需求清單 #### Step 2. 新增測試,測試 Fail #### Step 3. 撰寫剛好能通過測試的程式 #### Step 4. 重構程式 ## 範例 - Function ### Target Function - validatePassword 要開發一個密碼驗證的函式,會先請列一下密碼要被滿足的條件。 **需求清單**: * Test 1 - 長度至少 8 碼 * Test 2 - 至少含 1 個數字 * Test 3 - 至少含 1 個英文字母(大寫/小寫) ### Fixture 先準備好測試環境 ```js= // validate.js /** * 密碼驗證 * @param string $paramter ,ex: 'a12345678' * @return boolean 回傳結果 true 有效密碼, false 無效密碼 * @example validatePassword('a12345678') => true */ const validatePassword = () => {}; export default validatePassword; ``` ```js= // validate.spec.js describe("validatePassword", () => { // 1. 至少 8 碼 // 2. 至少含 1 個數字 // 3. 至少含 1 個英文字母(大寫/小寫) }); ``` ![TDD Function fixture](https://i.imgur.com/FcZOQUS.png) ### TDD cycle #### Red - `[ Test 0 防呆 ]` 在驗證的時候,基本都會有個防呆,只要值是 `""`、`undefined`、`null`,永遠都會被視為 `invalid value`,在第一行就會被 return 了,程式不會再往下跑,所以我們可以把這當作最簡單的開局測試案例。 * 在 `validate.spec.js` 新增傳入空字串要返回 `false` 的測試。 * `yarn test` => **fail test** ```js= // validate.spec.js it("return false given an empty string", () => { expect(validatePassword("")).toBe(false); }); ``` ![TDD Red 0](https://i.imgur.com/lrwP0l1.png) #### Green - `[Test 0 防呆 ]` * 新增無腦解,呼叫 `validatePassword` 直接 `return false` * `yarn test` => **pass test** ```js= // validate.js const validatePassword = () => { return false; }; ``` ![TDD Green 0](https://i.imgur.com/Wa1kiNC.png) #### Refactor - `[ Test 0 防呆 ]` Skip,目前還沒有重構必要。 --- #### Red - `[ Test 1 長度至少 8 碼 ]` * 在 `validate.spec.js` 中,新增密碼長度要大於 8 碼的測試 * `yarn test` => **fail test** :::info :information_source: 這邊第一個一定會過測試參數 `a12345678`,建議可以直接使用 PM 規格書提供的成功範例當做測試參數,永遠保證這個一定是合法密碼。 ::: ```js= // validate.spec.js it("at least 8 characters long", () => { expect(validatePassword("a12345678")).toBe(true); }); ``` ![TDD Red 1](https://i.imgur.com/95MNuxI.png) #### Green - `[ Test 1 長度至少 8 碼 ]` * `validate.js` 新增檢查參數大於 8 的判斷式 * `yarn test` => **pass test** ```js= // validate.js const validatePassword = (password) => { if (password.length >= 8) { return true; } return false; }; ``` ![TDD Green 1](https://i.imgur.com/5HZodu7.png) #### Refactor - `[ Test 1 長度至少 8 碼 ]` Skip,目前還沒有重構必要,程式碼還算易懂。 --- #### Red - `[ Test 2 至少含 1 個數字 ]` * 在 `validate.spec.js` 中,新增是否含數字的測試 * `yarn test` => **fail test** ```js= // validate.spec.js it("contains at least one number", () => { expect(validatePassword("ABCDEFGHIJ")).toBe(false); }); ``` ![TDD Red 2](https://i.imgur.com/txLSZdu.png) #### Green - `[ Test 2 至少含 1 個數字 ]` * `validate.js` 新增檢查數字的正則 * `yarn test` => **pass test** ```js= // validate.js const validatePassword = (password) => { if (password.length >= 8 && /[0-9]/g.test(password)) { return true; } return false; }; ``` :::info :information_source: 正則測試方法可參考 [MDN - `regexObj.test(str)`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) ::: ![TDD Green 2](https://i.imgur.com/4UdIAWx.png) #### Refactor - `[ Test 2 至少含 1 個數字 ]` * 目前 `長度大於 8 碼` 和 `數字正則` 擠在同一個 if 判斷式裡,降低易讀性,可將兩個拆開,各用變數做代表做重構 * `yarn test` => **pass test** ```js= // validate.js const validatePassword = (password) => { const validLength = password.length >= 8; const containsNumber = /[0-9]/g.test(password); return validLength && containsNumber; }; ``` ![TDD Refactor 2](https://i.imgur.com/fDfWhnC.png) --- #### Red - `[ Test 3 至少含 1 個英文字母(大寫/小寫) ]` * 在 `validate.spec.js` 中,新增是否含英文字母的測試 * `yarn test` => **fail test** ```js= // validate.spec.js it("contains at least one letter", () => { expect(validatePassword("123456789")).toBe(false); }); ``` ![TDD RED 3](https://i.imgur.com/OsrfSHE.png) #### Green - `[ Test 3 至少含 1 個英文字母(大寫/小寫) ]` * `validate.js` 新增檢查英文字母的正則 * `yarn test` => **pass test** ```js= // validate.js const validatePassword = (password) => { const validLength = password.length >= 8; const containsNumber = /[0-9]/g.test(password); const containsLetter = /[a-z]/g.test(password); return validLength && containsNumber && containsLetter; }; ``` ![TDD Green 3-1](https://i.imgur.com/Qd1snYu.png) 因為英文字母這邊當初條件有列,要可接受大小寫,我們可以多增加幾個 assert,以確保需求條件都滿足。 * 在 `validate.spec.js` 中,新增內有含以**小寫英文字母的參數**的測試 * `yarn test` => **pass test** 目前程式可接受小寫英文字母輸入的,接著試另一個大寫英文字母的測試案例。 ```js= // validate.spec.js it("contains at least one letter", () => { expect(validatePassword("123456789")).toBe(false); expect(validatePassword("a123456789")).toBe(true); }); ``` ![TDD Green 3-2](https://i.imgur.com/SX3sNXK.png) #### Red - `[ Test 3 至少含 1 個英文字母(大寫/小寫) ]` * 在 `validate.spec.js` 中,新增內有含以**大寫英文字母的參數**的測試 * `yarn test` => **fail test** 這是測試居然失敗!這時候就會回頭檢視功能程式,是不是沒考慮到英文字母為大寫的情境。 ```js= // validate.spec.js it("contains at least one letter", () => { expect(validatePassword("123456789")).toBe(false); expect(validatePassword("a123456789")).toBe(true); expect(validatePassword("A123456789")).toBe(true); }); ``` ![TDD Red 3-2](https://i.imgur.com/1FpS334.png) #### Green - `[ Test 3 至少含 1 個英文字母(大寫/小寫) ]` * `validate.js` 補上檢查大寫英文字母的正則 * `yarn test` => **pass test** 驗證通過後,以 TDD 流程開發的密碼驗證函式就完成了 :clap: ```js= // validate.js const containsLetter = /[aA-zZ]/g.test(password); ``` ![TDD Green 3-3](https://i.imgur.com/V3G0JDb.png) ### Final Code ```js= // validate.js const validatePassword = (password) => { const validLength = password.length >= 8; const containsNumber = /[0-9]/g.test(password); const containsLetter = /[aA-zZ]/g.test(password); return validLength && containsNumber && containsLetter; }; export default validatePassword; ``` ```js= // validate.spec.js import validatePassword from "./validate"; describe("validatePassword", () => { // 1. 至少 8 碼 // 2. 至少含 1 個數字 // 3. 至少含 1 個英文字母(大寫/小寫) it("return false given an empty string", () => { expect(validatePassword("")).toBe(false); }); it("at least 8 characters long", () => { expect(validatePassword("a12345678")).toBe(true); }); it("contains at least one number", () => { expect(validatePassword("ABCDEFGHIJ")).toBe(false); }); it("contains at least one letter", () => { expect(validatePassword("123456789")).toBe(false); expect(validatePassword("a123456789")).toBe(true); expect(validatePassword("A123456789")).toBe(true); }); }); ``` ![Final Code - Function](https://i.imgur.com/d3bAwPZ.png) * 技術使用: vue-test-utils + Jest * 範例來源:[TDD in JavaScript | Test Driven Development](https://www.youtube.com/watch?v=89Pl2Uok8xc) ## 範例 - Dom ### Target Dom - rating stars 一個顯示星等的 component,下方有顯示其分數。 **需求清單**: * Test 1 - 五個星星 * Test 2 - 星星 active 數量 * Test 3 - 顯示的數字 ![](https://i.imgur.com/ILwofUB.png) ### Fixture 準備好組件`(Rating.vue)`和測試檔案`(Rating.spec.js)`,並寫一些測試的起手式 code。 > **`wrapper`**:代表 Dom > **`beforeEach`**:每次測試時都會執行,可在此階段建立測試資料、Dom > **`afterEach`**:每次測試後都會執行,可在此階段移除測試資料、Dom > > 上面方法都是 **Jest** 提供的,可以參考 [Jest - Setup and Teardown](https://jestjs.io/docs/setup-teardown)。 ```js= // Rating.vue <template> <div></div> </template> <script></script> ``` ```js= // Rating.spec.js import { shallowMount } from "@vue/test-utils"; import Rating from "@/components/Rating.vue"; let wrapper = null; // mount component before each test beforeEach(() => { wrapper = shallowMount(Rating); }); // destroy component after each test afterEach(() => { wrapper.destroy(); }); describe("Rating", () => { it("renders the stars",() => {}) }); ``` ![TDD Vue](https://i.imgur.com/HLiP41E.png) ### TDD cycle #### Red - `[ Test 1 五個星星 ]` * 在 `Rating.spec.js` 中,新增有 **className `.star` 數量要有五個**的測試。 * `yarn test` => **fail test** ```js= // Rating.spec.js describe("Rating", () => { it("renders the stars", () => { const stars = wrapper.findAll(".star"); expect(stars.length).toBe(5); }); }); ``` ![TDD Vue Red](https://i.imgur.com/acGeVie.png) #### Green - `[ Test 1 五個星星 ]` * 新增無腦解 `<li class="star">` *5 * `yarn test` => **pass test** ```js= // Rating.vue <template> <div> <ul> <li class="star" /> <li class="star" /> <li class="star" /> <li class="star" /> <li class="star" /> </ul> </div> </template> ``` ![TDD Vue Green](https://i.imgur.com/QsneYEm.png) #### Refactor - `[ Test 1 五個星星 ]` 如果今天需求改成要 10 顆星星呢?再複製五個 `<li>` 貼上?那實在太對不起工程師這職業了,所以我們要把這段 hard code 重構一下,讓最大星星數量由外面當作 `props` 傳進來。 * `Rating.vue` 新增 `props` 參數 `maxStars` * `Rating.vue` 中的 `<li>` 用 `v-for` 改寫 * `Rating.spec.js` 再 Mount 時傳入 `propsData` * `yarn test` => **pass test** 保持通過測試狀態!Awesome!第一個 `TDD cycle` 就完成了:clap: 接下來就再依照需求清單,進行下一個 `Red-Green-Refactor`。 ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" /> </ul> </div> </template> <script> export default { props: { maxStars: { type: Number, default: 0 } } }; </script> ``` ```js= // Rating.spec.js beforeEach(() => { wrapper = shallowMount(Rating, { propsData: { maxStars: 5 } }); }); ``` ![TDD Vue Refactor](https://i.imgur.com/G3dPayZ.png) --- #### Red - `[ Test 2 星星 active 數量 ]` * 在 `Rating.spec.js` 中,新增 3 個 `active` 星星數的測試 * `yarn test` => **fail test** ```js= // Rating.spec.js describe("Rating", () => { it("renders the stars", () => { const stars = wrapper.findAll(".star"); expect(stars.length).toBe(5); }); it("renders the active stars", () => { const active = wrapper.findAll(".star.active"); expect(active.length).toBe(3); }); }); ``` ![TDD Vue Red 2](https://i.imgur.com/Xnl9kC5.png) #### Green - `[ Test 2 星星 active 數量 ]` * 在 `Rating.vue` 中,className 條件判斷寫 `star <= 3` 時,className 會自動新增 `active` * `yarn test` => **pass test** ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= 3 }" /> </ul> </div> </template> ``` ![TDD Vue Green 2](https://i.imgur.com/yuHnCaW.png) #### Refactor - `[ Test 2 星星 active 數量 ]` active 星數,一般是由使用者或後端產生的,所以一樣要改成由外面用 props 方式傳入。 * 在 `Rating.vue`、`Rating.spec.js` 新增 `props` 參數 `initGrade` * className 的條件判斷,改用參數 * `yarn test` => **pass test** ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= initGrade }" /> </ul> </div> </template> <script> export default { props: { maxStars: { type: Number, default: 0 }, initGrade: { type: Number, default: 0 } } }; </script> ``` ```js= // Rating.spec.js beforeEach(() => { wrapper = shallowMount(Rating, { propsData: { maxStars: 5, initGrade: 3 } }); }); ``` ![TDD Vue Refactor 2](https://i.imgur.com/7Z3c3bm.png) --- #### Red - `[Test 3 顯示的數字]` 最後一個測試了,下方需顯示當前 active 星數和全部星數的資訊。 * 在 `Rating.spec.js` 中新增測試,條件為一個 `.summary` 的 dom,內容文字為前星數的資訊 * `yarn test` => **fail test** ```js= // Rating.spec.js describe("Rating", () => { it("renders the stars", () => { const stars = wrapper.findAll(".star"); expect(stars.length).toBe(5); }); it("renders the active stars", () => { const active = wrapper.findAll(".star.active"); expect(active.length).toBe(3); }); it("renders a summary", () => { const summary = wrapper.find(".summary"); expect(summary.text()).toBe("2 of 5"); }); }); ``` ![TDD Vue Red 3](https://i.imgur.com/svpVyJT.png) >[vue-test-utils]: find did not return .summary, cannot call text() on empty Wrapper[color=#ff0000] > >意即在目前組件內找不到 `.summary` 的 dom,無法呼叫 text() 方法。 #### Green - `[ Test 3 顯示的數字 ]` * 先新增 `<div class="summary" />` * `yarn test` => **fail test**,雖然還沒通過測試,但這邊也可以發現 error message 已經改變了,可以找到 `.summary`,只是文字訊息不對。 ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= initGrade }" /> </ul> <div class="summary" /> </div> </template> ``` ![TDD Vue Green - 1](https://i.imgur.com/kdrV4eJ.png) >Expected: "2 of 5" Received: ""[color=#ff0000] * 在 `Rating.vue` 直接把文字寫跟測試案例一模一樣 `3 of 5` * `yarn test` => **pass test** ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= initGrade }" /> </ul> <div class="summary">3 of 5</div> </div> </template> ``` ![TDD Vue Green - 2](https://i.imgur.com/nQrsIdq.png) #### Refactor - `[ Test 3 顯示的數字 ]` * `.summary` 內的文字顯示改用 `props` 參數 * `yarn test` => **pass test** Bravo ! 整個 TDD 流程完成了! ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= initGrade }" /> </ul> <div class="summary">{{ initGrade }} of {{ maxStars }}</div> </div> </template> ``` ![TDD Vue Refactor](https://i.imgur.com/4iXT529.png) ### Final Code ```js= // Rating.vue <template> <div> <ul> <li v-for="star in maxStars" :key="star" class="star" :class="{ active: star <= initGrade }" /> </ul> <div class="summary">{{ initGrade }} of {{ maxStars }}</div> </div> </template> <script> export default { props: { maxStars: { type: Number, default: 0 }, initGrade: { type: Number, default: 0 } } }; </script> ``` ```js= // Rating.spec.js import { shallowMount } from "@vue/test-utils"; import Rating from "@/components/Rating.vue"; let wrapper = null; // mount component before each test beforeEach(() => { wrapper = shallowMount(Rating, { propsData: { maxStars: 5, initGrade: 3 } }); }); // destroy component after each test afterEach(() => { wrapper.destroy(); }); describe("Rating", () => { it("renders the stars", () => { const stars = wrapper.findAll(".star"); expect(stars.length).toBe(5); }); it("renders the active stars", () => { const active = wrapper.findAll(".star.active"); expect(active.length).toBe(3); }); it("renders a summary", () => { const summary = wrapper.find(".summary"); expect(summary.text()).toBe("3 of 5"); }); }); ``` ![TDD Result](https://i.imgur.com/LEanr6L.png) * 技術使用: vue-test-utils + Jest * 範例來源:[Test driven development with Vue.js by Sarah Dayan](https://www.youtube.com/watch?v=DD1fEhcEzY8) ###### tags: `test`、`TDD`

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully