├── .editorconfig ├── .env ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── health-check.yml │ ├── pull-request.yml │ └── test.yml ├── .gitignore ├── FRONTEND_INSTRUCTIONS.md ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── article.cy.ts │ ├── auth.cy.ts │ ├── constant.ts │ ├── favorite.cy.ts │ ├── follow.cy.ts │ ├── home.cy.ts │ ├── tag.cy.ts │ └── tsconfig.json ├── fixtures │ ├── article-comments.json │ ├── article.json │ ├── articles-of-tag.json │ ├── articles.json │ ├── profile.json │ ├── tags.json │ └── user.json └── support │ ├── commands.ts │ └── e2e.ts ├── eslint.config.js ├── index.html ├── logo.png ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── src ├── App.vue ├── components │ ├── AppFooter.spec.ts │ ├── AppFooter.vue │ ├── AppLink.spec.ts │ ├── AppLink.vue │ ├── AppNavigation.spec.ts │ ├── AppNavigation.vue │ ├── AppPagination.spec.ts │ ├── AppPagination.vue │ ├── ArticleDetail.spec.ts │ ├── ArticleDetail.vue │ ├── ArticleDetailComment.spec.ts │ ├── ArticleDetailComment.vue │ ├── ArticleDetailComments.spec.ts │ ├── ArticleDetailComments.vue │ ├── ArticleDetailCommentsForm.spec.ts │ ├── ArticleDetailCommentsForm.vue │ ├── ArticleDetailMeta.spec.ts │ ├── ArticleDetailMeta.vue │ ├── ArticlesList.spec.ts │ ├── ArticlesList.vue │ ├── ArticlesListArticlePreview.spec.ts │ ├── ArticlesListArticlePreview.vue │ ├── ArticlesListNavigation.spec.ts │ ├── ArticlesListNavigation.vue │ ├── PopularTags.spec.ts │ ├── PopularTags.vue │ └── __snapshots__ │ │ └── ArticleDetail.spec.ts.snap ├── composable │ ├── useArticles.ts │ ├── useFavoriteArticle.ts │ ├── useFollowProfile.ts │ ├── useProfile.ts │ └── useTags.ts ├── config.ts ├── env.d.ts ├── main.ts ├── pages │ ├── Article.spec.ts │ ├── Article.vue │ ├── EditArticle.spec.ts │ ├── EditArticle.vue │ ├── Home.vue │ ├── Login.spec.ts │ ├── Login.vue │ ├── Profile.spec.ts │ ├── Profile.vue │ ├── Register.spec.ts │ ├── Register.vue │ ├── Settings.spec.ts │ └── Settings.vue ├── plugins │ ├── global-components.ts │ ├── marked.ts │ └── set-authorization-token.ts ├── router.ts ├── services │ ├── api.ts │ ├── index.ts │ └── openapi.yml ├── setupTests.ts ├── store │ └── user.ts ├── types │ ├── global-component.d.ts │ └── global.d.ts └── utils │ ├── filters.spec.ts │ ├── filters.ts │ ├── params-to-query.spec.ts │ ├── params-to-query.ts │ ├── storage.spec.ts │ ├── storage.ts │ ├── test │ ├── fixtures.ts │ └── test.utils.ts │ ├── use-async.spec.ts │ └── use-async.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BASE_URL=/ 2 | VITE_API_HOST=https://api.realworld.io 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | allow: 13 | - dependency-type: production 14 | 15 | - package-ecosystem: github-actions 16 | directory: .github/workflows 17 | schedule: 18 | interval: monthly 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: CodeQL 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: '43 3 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [javascript-typescript] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | # - run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/health-check.yml: -------------------------------------------------------------------------------- 1 | name: Check Swagger API 2 | 3 | on: 4 | schedule: 5 | - cron: '35 17 */4 * 4' 6 | 7 | env: 8 | CI: true 9 | 10 | jobs: 11 | e2e_tests: 12 | name: E2E test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | version: 9 20 | run_install: false 21 | 22 | # https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping 23 | - name: Use Node.js 22.x 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22.x 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: E2E test 33 | run: pnpm test:e2e:prod 34 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | run_install: false 22 | 23 | - name: Use Node.js 22.x 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22.x 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install --no-frozen-lockfile 31 | 32 | - name: TypeScript check 33 | run: pnpm lint 34 | 35 | - name: Eslint check 36 | run: pnpm lint 37 | 38 | unit_test: 39 | name: Unit test 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - uses: pnpm/action-setup@v4 45 | with: 46 | version: 9 47 | run_install: false 48 | 49 | - name: Use Node.js 22.x 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 22.x 53 | cache: pnpm 54 | 55 | - name: Install dependencies 56 | run: pnpm install --no-frozen-lockfile 57 | 58 | - name: Unit test 59 | run: pnpm test:unit 60 | 61 | - name: Update coverage report 62 | uses: codecov/codecov-action@v4 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | 66 | e2e_tests: 67 | name: E2E test 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - uses: pnpm/action-setup@v4 73 | with: 74 | version: 9 75 | run_install: false 76 | 77 | - name: Use Node.js 22.x 78 | uses: actions/setup-node@v4 79 | with: 80 | node-version: 22.x 81 | cache: pnpm 82 | 83 | - name: Install dependencies 84 | run: pnpm install --no-frozen-lockfile 85 | 86 | - name: Get cypress version 87 | id: cypress-version 88 | run: echo "version=$(pnpm info cypress version)" >> $GITHUB_OUTPUT 89 | 90 | - name: Cache cypress binary 91 | id: cache-cypress-binary 92 | uses: actions/cache@v4 93 | with: 94 | path: ~/.cache/Cypress 95 | key: cypress-binary-${{ runner.os }}-${{ steps.cypress-version.outputs.version }} 96 | 97 | - name: Install cypress binary 98 | if: steps.cache-cypress-binary.outputs.cache-hit != 'true' 99 | run: pnpm cypress install 100 | 101 | - name: E2E test 102 | run: pnpm test:e2e:ci 103 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | run_install: false 22 | 23 | - name: Use Node.js 22.x 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22.x 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: TypeScript check 33 | run: pnpm type-check 34 | 35 | - name: Eslint check 36 | run: pnpm lint 37 | 38 | unit_test: 39 | name: Unit test 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - uses: pnpm/action-setup@v4 45 | with: 46 | version: 9 47 | run_install: false 48 | 49 | - name: Use Node.js 22.x 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 22.x 53 | cache: pnpm 54 | 55 | - name: Install dependencies 56 | run: pnpm install 57 | 58 | - name: Unit test 59 | run: pnpm test:unit 60 | 61 | - name: Update coverage report 62 | uses: codecov/codecov-action@v4 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | 66 | e2e_tests: 67 | name: E2E test 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - uses: pnpm/action-setup@v4 73 | with: 74 | version: 9 75 | run_install: false 76 | 77 | - name: Use Node.js 22.x 78 | uses: actions/setup-node@v4 79 | with: 80 | node-version: 22.x 81 | cache: pnpm 82 | 83 | - name: Install dependencies 84 | run: pnpm install 85 | 86 | - name: Get cypress version 87 | id: cypress-version 88 | run: echo "version=$(pnpm info cypress version)" >> $GITHUB_OUTPUT 89 | 90 | - name: Cache cypress binary 91 | id: cache-cypress-binary 92 | uses: actions/cache@v4 93 | with: 94 | path: ~/.cache/Cypress 95 | key: cypress-binary-${{ runner.os }}-${{ steps.cypress-version.outputs.version }} 96 | 97 | - name: Install cypress binary 98 | if: steps.cache-cypress-binary.outputs.cache-hit != 'true' 99 | run: pnpm cypress install 100 | 101 | - name: E2E test 102 | run: pnpm test:e2e:ci 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /FRONTEND_INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | > *Note: Delete this file before publishing your app!* 2 | 3 | ### Using the hosted API 4 | 5 | Simply point your [API requests](https://github.com/gothinkster/realworld/tree/master/api) to `https://api.realworld.io/api` and you're good to go! 6 | 7 | ### Routing Guidelines 8 | 9 | - Home page (URL: /#/ ) 10 | - List of tags 11 | - List of articles pulled from either Feed, Global, or by Tag 12 | - Pagination for list of articles 13 | - Sign in/Sign up pages (URL: /#/login, /#/register ) 14 | - Uses JWT (store the token in localStorage) 15 | - Authentication can be easily switched to session/cookie based 16 | - Settings page (URL: /#/settings ) 17 | - Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here ) 18 | - Article page (URL: /#/article/article-slug-here ) 19 | - Delete article button (only shown to article's author) 20 | - Render markdown from server client side 21 | - Comments section at bottom of page 22 | - Delete comment button (only shown to comment's author) 23 | - Profile page (URL: /#/profile/:username, /#/profile/:username/favorites ) 24 | - Show basic user info 25 | - List of articles populated from author's created articles or author's favorited articles 26 | 27 | # Styles 28 | 29 | Instead of having the Bootstrap theme included locally, we recommend loading the precompiled theme from our CDN (our [header template](#header) does this by default): 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template). 36 | 37 | # Templates 38 | 39 | - [Layout](#layout) 40 | - [Header](#header) 41 | - [Footer](#footer) 42 | - [Pages](#pages) 43 | - [Home](#home) 44 | - [Login/Register](#loginregister) 45 | - [Profile](#profile) 46 | - [Settings](#settings) 47 | - [Create/Edit Article](#createedit-article) 48 | - [Article](#article) 49 | 50 | ## Layout 51 | 52 | ### Header 53 | 54 | ```html 55 | 56 | 57 | 58 | 59 | Conduit 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 92 | 93 | ``` 94 | 95 | ### Footer 96 | ```html 97 | 105 | 106 | 107 | 108 | ``` 109 | 110 | ## Pages 111 | 112 | ### Home 113 | ```html 114 |
115 | 116 | 122 | 123 |
124 |
125 | 126 |
127 |
128 | 136 |
137 | 138 |
139 | 149 | 150 |

How to build webapps that scale

151 |

This is the description for the post.

152 | Read more... 153 |
154 |
155 | 156 |
157 | 167 | 168 |

The song you won't ever stop singing. No matter how hard you try.

169 |

This is the description for the post.

170 | Read more... 171 |
172 |
173 | 174 |
175 | 176 |
177 | 191 |
192 | 193 |
194 |
195 | 196 |
197 | ``` 198 | 199 | ### Login/Register 200 | 201 | ```html 202 |
203 |
204 |
205 | 206 |
207 |

Sign up

208 |

209 | Have an account? 210 |

211 | 212 |
    213 |
  • That email is already taken
  • 214 |
215 | 216 |
217 |
218 | 219 |
220 |
221 | 222 |
223 |
224 | 225 |
226 | 229 |
230 |
231 | 232 |
233 |
234 |
235 | ``` 236 | 237 | ### Profile 238 | 239 | ```html 240 |
241 | 242 |
243 |
244 |
245 | 246 |
247 | 248 |

Eric Simons

249 |

250 | Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from the Hunger Games 251 |

252 | 257 |
258 | 259 |
260 |
261 |
262 | 263 |
264 |
265 | 266 |
267 |
268 | 276 |
277 | 278 |
279 | 289 | 290 |

How to build webapps that scale

291 |

This is the description for the post.

292 | Read more... 293 |
294 |
295 | 296 |
297 | 307 | 308 |

The song you won't ever stop singing. No matter how hard you try.

309 |

This is the description for the post.

310 | Read more... 311 |
    312 |
  • Music
  • 313 |
  • Song
  • 314 |
315 |
316 |
317 | 318 |
319 | 320 |
321 |
322 | 323 |
324 | ``` 325 | 326 | ### Settings 327 | 328 | ```html 329 |
330 |
331 |
332 | 333 |
334 |

Your Settings

335 | 336 |
337 |
338 |
339 | 340 |
341 |
342 | 343 |
344 |
345 | 346 |
347 |
348 | 349 |
350 |
351 | 352 |
353 | 356 |
357 |
358 |
359 | 360 |
361 |
362 |
363 | ``` 364 | 365 | ### Create/Edit Article 366 | 367 | ```html 368 |
369 |
370 |
371 | 372 |
373 |
374 |
375 |
376 | 377 |
378 |
379 | 380 |
381 |
382 | 383 |
384 |
385 |
386 |
387 | 390 |
391 |
392 |
393 | 394 |
395 |
396 |
397 | 398 | ``` 399 | 400 | ### Article 401 | 402 | ```html 403 |
404 | 405 | 431 | 432 |
433 | 434 |
435 |
436 |

437 | Web development technologies have evolved at an incredible clip over the past few years. 438 |

439 |

Introducing RealWorld.

440 |

It's a great solution for learning how other frameworks work.

441 |
442 |
443 | 444 |
445 | 446 |
447 | 466 |
467 | 468 |
469 | 470 |
471 | 472 |
473 |
474 | 475 |
476 | 482 |
483 | 484 |
485 |
486 |

With supporting text below as a natural lead-in to additional content.

487 |
488 | 496 |
497 | 498 |
499 |
500 |

With supporting text below as a natural lead-in to additional content.

501 |
502 | 514 |
515 | 516 |
517 | 518 |
519 | 520 |
521 | 522 |
523 | ``` 524 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dongsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld Example App](logo.png) 2 | 3 | [![Codecov branch](https://img.shields.io/codecov/c/github/mutoe/vue3-realworld-example-app/master?logo=codecov&style=for-the-badge)](https://app.codecov.io/gh/mutoe/vue3-realworld-example-app/branch/master) 4 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/mutoe/vue3-realworld-example-app/test.yml?logo=github&style=for-the-badge)](https://github.com/mutoe/vue3-realworld-example-app/actions?query=branch%3Amaster) 5 | [![code style](https://img.shields.io/badge/Code_Style-Anthony_Fu-333?style=for-the-badge&logo=eslint)](https://github.com/anthony/eslint-config) 6 | 7 | > ### [Vue3](https://v3.vuejs.org/) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 8 | 9 | - [Demo](https://vue3-realworld-example-app-mutoe.vercel.app) 10 | - [RealWorld](https://github.com/gothinkster/realworld) 11 | 12 | This codebase was created to demonstrate a fully fledged fullstack application built with **Vue3** including CRUD operations, authentication, routing, pagination, and more. 13 | 14 | We've gone to great lengths to adhere to the **Vue3** community styleguides & best practices. 15 | 16 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 17 | 18 | # What works? 19 | 20 | - [x] [Vite](https://github.com/vitejs/vite) 21 | - [x] [Composition API](https://composition-api.vuejs.org/) 22 | - [x] [SFC \ 17 | 18 | 19 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutoe/vue3-realworld-example-app/6dbfbe9d03df96e7c264ea55674529550a2664cc/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-realworld-example-app", 3 | "private": true, 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "prepare": "simple-git-hooks", 8 | "dev": "vite", 9 | "build": "vite build", 10 | "serve": "vite preview --port 4173", 11 | "type-check": "vue-tsc --noEmit", 12 | "lint": "eslint --fix .", 13 | "test": "npm run test:unit && npm run test:e2e:ci", 14 | "test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4173\"", 15 | "test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4173\"", 16 | "test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173", 17 | "test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app", 18 | "test:unit": "vitest run", 19 | "generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts" 20 | }, 21 | "dependencies": { 22 | "insane": "^2.6.2", 23 | "marked": "^14.0.0", 24 | "pinia": "^2.2.1", 25 | "vue": "^3.4.37", 26 | "vue-router": "^4.4.3" 27 | }, 28 | "devDependencies": { 29 | "@mutoe/eslint-config": "^2.8.3", 30 | "@pinia/testing": "^0.1.5", 31 | "@testing-library/cypress": "^10.0.2", 32 | "@testing-library/user-event": "^14.5.2", 33 | "@testing-library/vue": "^8.1.0", 34 | "@vitejs/plugin-vue": "^5.1.2", 35 | "@vitest/coverage-v8": "^2.0.5", 36 | "concurrently": "^8.2.2", 37 | "cypress": "^13.13.2", 38 | "eslint": "^8.57.0", 39 | "eslint-plugin-cypress": "^3.4.0", 40 | "eslint-plugin-vitest": "^0.5.4", 41 | "eslint-plugin-vue": "^9.27.0", 42 | "happy-dom": "^14.12.3", 43 | "lint-staged": "^15.2.8", 44 | "msw": "^2.3.5", 45 | "rollup-plugin-analyzer": "^4.0.0", 46 | "simple-git-hooks": "^2.11.1", 47 | "swagger-typescript-api": "^13.0.16", 48 | "typescript": "~5.5.4", 49 | "vite": "^5.4.0", 50 | "vitest": "^2.0.5", 51 | "vitest-dom": "^0.1.1", 52 | "vue-tsc": "^2.0.29" 53 | }, 54 | "simple-git-hooks": { 55 | "pre-commit": "npm exec lint-staged", 56 | "pre-push": "npm run lint && npm run build" 57 | }, 58 | "lint-staged": { 59 | "*": "eslint --fix" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutoe/vue3-realworld-example-app/6dbfbe9d03df96e7c264ea55674529550a2664cc/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /src/components/AppFooter.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import { renderOptions } from 'src/utils/test/test.utils' 4 | import AppFooter from './AppFooter.vue' 5 | 6 | describe('# AppFooter', () => { 7 | it('should render correctly', () => { 8 | const { container } = render(AppFooter, renderOptions()) 9 | 10 | expect(container).toHaveTextContent('Real world app') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/components/AppLink.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, waitFor } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import { renderOptions } from 'src/utils/test/test.utils.ts' 4 | import AppLink from './AppLink.vue' 5 | 6 | describe('# AppLink', () => { 7 | it('should redirect to another page when click the link', async () => { 8 | const { container, getByRole } = render(AppLink, renderOptions({ 9 | props: { name: 'tag', params: { tag: 'foo' } }, 10 | slots: { default: () => 'Go to Foo tag' }, 11 | })) 12 | 13 | expect(container).toHaveTextContent('Go to Foo tag') 14 | await fireEvent.click(getByRole('link', { name: 'tag' })) 15 | 16 | await waitFor(() => expect(getByRole('link', { name: 'tag' })).toHaveClass('router-link-active')) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/AppLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/components/AppNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import { renderOptions } from 'src/utils/test/test.utils.ts' 4 | import AppNavigation from './AppNavigation.vue' 5 | 6 | describe('# AppNavigation', () => { 7 | it('should render Sign in and Sign up when user not logged', () => { 8 | const { getByRole } = render(AppNavigation, renderOptions()) 9 | 10 | expect(getByRole('link', { name: 'Home' })).toHaveTextContent('Home') 11 | expect(getByRole('link', { name: 'Sign in' })).toHaveTextContent('Sign in') 12 | expect(getByRole('link', { name: 'Sign up' })).toHaveTextContent('Sign up') 13 | }) 14 | 15 | it('should render xxx when user logged', () => { 16 | const { getByRole } = render(AppNavigation, renderOptions({ 17 | initialState: { 18 | user: { user: { username: 'username', email: '', token: '', bio: '', image: '' } }, 19 | }, 20 | })) 21 | 22 | expect(getByRole('link', { name: 'Home' })).toHaveTextContent('Home') 23 | expect(getByRole('link', { name: 'New Post' })).toHaveTextContent('New Post') 24 | expect(getByRole('link', { name: 'Settings' })).toHaveTextContent('Settings') 25 | expect(getByRole('link', { name: 'username' })).toHaveTextContent('username') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/AppNavigation.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 96 | -------------------------------------------------------------------------------- /src/components/AppPagination.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/vue' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import { renderOptions } from 'src/utils/test/test.utils.ts' 4 | import AppPagination from './AppPagination.vue' 5 | 6 | describe('# AppPagination', () => { 7 | it('should highlight current active page', () => { 8 | const { getByRole } = render(AppPagination, renderOptions({ 9 | props: { page: 1, count: 15 }, 10 | })) 11 | 12 | expect(getByRole('link', { name: 'Go to page 1' }).parentNode).toHaveClass('active') 13 | expect(getByRole('link', { name: 'Go to page 2' }).parentNode).not.toHaveClass('active') 14 | }) 15 | 16 | it('should call onPageChange when click a page item', async () => { 17 | const onPageChange = vi.fn() 18 | const { getByRole } = render(AppPagination, renderOptions({ 19 | props: { page: 1, count: 15, onPageChange }, 20 | })) 21 | 22 | await fireEvent.click(getByRole('link', { name: 'Go to page 2' })) 23 | 24 | expect(onPageChange).toHaveBeenCalledWith(2) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/AppPagination.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/components/ArticleDetail.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import fixtures from 'src/utils/test/fixtures' 4 | import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils' 5 | import ArticleDetail from './ArticleDetail.vue' 6 | 7 | describe('# ArticleDetail', () => { 8 | const server = setupMockServer( 9 | ['/api/articles/markdown', { article: { ...fixtures.article, body: fixtures.markdown } }], 10 | ['/api/articles/markdown-cn', { article: { ...fixtures.article, body: fixtures.markdownCN } }], 11 | ['/api/articles/markdown-xss', { article: { ...fixtures.article, body: fixtures.markdownXss } }], 12 | ) 13 | 14 | it('should render markdown body correctly', async () => { 15 | const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({ 16 | initialRoute: { name: 'article', params: { slug: 'markdown' } }, 17 | })) 18 | await server.waitForRequest('GET', '/api/articles/markdown') 19 | 20 | expect(container.querySelector('#article-content')).toMatchSnapshot() 21 | }) 22 | 23 | it('should render markdown (zh-CN) body correctly', async () => { 24 | const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({ 25 | initialRoute: { name: 'article', params: { slug: 'markdown-cn' } }, 26 | })) 27 | await server.waitForRequest('GET', '/api/articles/markdown-cn') 28 | 29 | expect(container.querySelector('#article-content')).toMatchSnapshot() 30 | }) 31 | 32 | it('should filter the xss content in Markdown body', async () => { 33 | const { container } = render(asyncWrapper(ArticleDetail), await renderOptions({ 34 | initialRoute: { name: 'article', params: { slug: 'markdown-xss' } }, 35 | })) 36 | await server.waitForRequest('GET', '/api/articles/markdown-xss') 37 | 38 | expect(container.querySelector('#article-content')).toMatchSnapshot() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/components/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 62 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComment.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/vue' 2 | import { describe, expect, it, vi } from 'vitest' 3 | import fixtures from 'src/utils/test/fixtures' 4 | import { renderOptions } from 'src/utils/test/test.utils.ts' 5 | import ArticleDetailComment from './ArticleDetailComment.vue' 6 | 7 | describe('# ArticleDetailComment', () => { 8 | it('should render correctly', () => { 9 | const { container, queryByRole } = render(ArticleDetailComment, renderOptions({ 10 | props: { comment: fixtures.comment }, 11 | })) 12 | 13 | expect(container).toHaveTextContent('Comment body') 14 | expect(container).toHaveTextContent('1/1/2020') 15 | expect(queryByRole('button', { name: 'Delete comment' })).not.toBeInTheDocument() 16 | }) 17 | 18 | it('should delete comment button when comment author is same user', () => { 19 | const { getByRole } = render(ArticleDetailComment, renderOptions({ 20 | props: { 21 | comment: fixtures.comment, 22 | username: fixtures.author.username, 23 | }, 24 | })) 25 | 26 | expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument() 27 | }) 28 | 29 | it('should emit remove comment when click remove comment button', async () => { 30 | const onRemoveComment = vi.fn() 31 | const { getByRole } = render(ArticleDetailComment, renderOptions({ 32 | props: { 33 | comment: fixtures.comment, 34 | username: fixtures.author.username, 35 | onRemoveComment, 36 | }, 37 | })) 38 | 39 | await fireEvent.click(getByRole('button', { name: 'Delete comment' })) 40 | 41 | expect(onRemoveComment).toHaveBeenCalled() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComment.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 65 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComments.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import fixtures from 'src/utils/test/fixtures' 4 | import { asyncWrapper, renderOptions, setupMockServer } from 'src/utils/test/test.utils' 5 | import ArticleDetailComments from './ArticleDetailComments.vue' 6 | 7 | describe('# ArticleDetailComments', () => { 8 | // const mockDeleteComment = deleteComment as jest.MockedFunction 9 | 10 | const server = setupMockServer( 11 | ['GET', '/api/profiles/*', { profile: fixtures.author }], 12 | ['GET', '/api/articles/*/comments', { comments: [fixtures.comment] }], 13 | ['POST', '/api/articles/*/comments', { comment: fixtures.comment2 }], 14 | ['DELETE', '/api/articles/*/comments/*'], 15 | ) 16 | 17 | it('should render correctly', async () => { 18 | const { container } = render(asyncWrapper(ArticleDetailComments), await renderOptions({ 19 | initialRoute: { name: 'article', params: { slug: fixtures.article.slug } }, 20 | initialState: { user: { user: null } }, 21 | })) 22 | 23 | await server.waitForRequest('GET', '/api/articles/article-foo/comments') 24 | 25 | expect(container).toHaveTextContent('Comment body') 26 | }) 27 | 28 | it('should display new comment when post new comment', async () => { 29 | const { container, getByRole } = render(asyncWrapper(ArticleDetailComments), await renderOptions({ 30 | initialRoute: { name: 'article', params: { slug: fixtures.article.slug } }, 31 | initialState: { user: { user: fixtures.user } }, 32 | })) 33 | await server.waitForRequest('GET', '/api/articles/*/comments') 34 | expect(container).toHaveTextContent('Comment body') 35 | 36 | await fireEvent.update(getByRole('textbox', { name: 'Write comment' }), fixtures.comment2.body) 37 | await fireEvent.click(getByRole('button', { name: 'Submit' })) 38 | 39 | await server.waitForRequest('POST', '/api/articles/*/comments') 40 | expect(container).toHaveTextContent(fixtures.comment2.body) 41 | }) 42 | 43 | it('should call remove comment service when click delete button', async () => { 44 | const { container, getByRole } = render(asyncWrapper(ArticleDetailComments), await renderOptions({ 45 | initialRoute: { name: 'article', params: { slug: fixtures.article.slug } }, 46 | initialState: { user: { user: fixtures.user } }, 47 | })) 48 | await server.waitForRequest('GET', '/api/articles/article-foo/comments') 49 | 50 | await fireEvent.click(getByRole('button', { name: 'Delete comment' })) 51 | 52 | await server.waitForRequest('DELETE', '/api/articles/article-foo/comments/*') 53 | expect(container).not.toHaveTextContent(fixtures.comment.body) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComments.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 46 | -------------------------------------------------------------------------------- /src/components/ArticleDetailCommentsForm.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/vue' 2 | import { describe, expect, it } from 'vitest' 3 | import ArticleDetailCommentsForm from 'src/components/ArticleDetailCommentsForm.vue' 4 | import fixtures from 'src/utils/test/fixtures' 5 | import { renderOptions, setupMockServer } from 'src/utils/test/test.utils.ts' 6 | 7 | describe('# ArticleDetailCommentsForm', () => { 8 | const server = setupMockServer( 9 | ['POST', '/api/articles/*/comments', { comment: { body: 'some texts...' } }], 10 | ) 11 | 12 | it('should display sign in button when user not logged', () => { 13 | const { container } = render(ArticleDetailCommentsForm, renderOptions({ 14 | initialState: { user: { user: null } }, 15 | props: { articleSlug: fixtures.article.slug }, 16 | })) 17 | 18 | expect(container).toHaveTextContent('add comments on this article') 19 | }) 20 | 21 | it('should display form when user logged', async () => { 22 | server.use(['GET', '/api/profiles/*', { profile: fixtures.author }]) 23 | const { getByRole } = render(ArticleDetailCommentsForm, renderOptions({ 24 | initialState: { user: { user: fixtures.user } }, 25 | props: { articleSlug: fixtures.article.slug }, 26 | })) 27 | await server.waitForRequest('GET', '/api/profiles/*') 28 | 29 | await fireEvent.update(getByRole('textbox', { name: 'Write comment' }), 'some texts...') 30 | await fireEvent.click(getByRole('button', { name: 'Submit' })) 31 | 32 | await server.waitForRequest('POST', '/api/articles/*/comments') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/components/ArticleDetailCommentsForm.vue: -------------------------------------------------------------------------------- 1 |