├── src ├── @types │ ├── vite-env.d.ts │ ├── charcuterie.d.ts │ ├── shims.d.ts │ └── charcuterie-enums.ts ├── js │ ├── index.ts │ ├── utils.ts │ └── charcuterie-inventory.ts ├── __tests__ │ ├── utils.test.ts │ ├── CharcuterieInventory.test.ts │ ├── CharcuterieBoard.test.ts │ ├── CharcuterieBoardLogo.test.ts │ ├── charcuterie-inventory.test.ts │ ├── CharcuterieItem.test.ts │ ├── App.test.ts │ └── __snapshots__ │ │ └── charcuterie-inventory.test.ts.snap ├── vue │ ├── components │ │ ├── CharcuterieInventory.vue │ │ ├── CharcuterieBoardLogo.vue │ │ ├── CharcuterieBoard.vue │ │ └── CharcuterieItem.vue │ └── App.vue ├── css │ └── style.css └── assets │ ├── cheese.svg │ ├── nut.svg │ ├── meat.svg │ ├── fruit.svg │ ├── vegetable.svg │ ├── cracker.svg │ └── charcuterie-board-logo.svg ├── .vscode └── extensions.json ├── Dockerfile ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .stylelintrc.json ├── .eslintrc ├── tsconfig.json ├── vite.config.ts ├── package.json ├── public └── vite.svg ├── Makefile └── README.md /src/@types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/@types/charcuterie.d.ts: -------------------------------------------------------------------------------- 1 | interface CharcuterieItem { 2 | name: string; 3 | type: CharcuterieItemType; 4 | calories?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/js/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import '../css/style.css' 3 | import App from '../vue/App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /src/js/utils.ts: -------------------------------------------------------------------------------- 1 | export const kebabCase = (string: string) => string 2 | .replace(/([a-z])([A-Z])/g, "$1-$2") 3 | .replace(/[\s_]+/g, '-') 4 | .toLowerCase(); 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG TAG=18-alpine 2 | FROM node:$TAG 3 | 4 | RUN npm install -g npm@^9.3.0 5 | 6 | WORKDIR /app/ 7 | 8 | CMD ["run build"] 9 | 10 | ENTRYPOINT ["npm"] 11 | -------------------------------------------------------------------------------- /src/@types/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/@types/charcuterie-enums.ts: -------------------------------------------------------------------------------- 1 | export enum CharcuterieItemType { 2 | Cheese = "cheese", 3 | Meat = "meat", 4 | Fruit = "fruit", 5 | Nut = "nut", 6 | Cracker = "cracker", 7 | Vegetable = "vegetable", 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import {it, expect, describe} from 'vitest'; 2 | import {kebabCase} from "@/js/utils"; 3 | 4 | describe('utils.ts', () => { 5 | it('should properly kebab-case passed in strings', () => { 6 | expect(kebabCase('RyansGoBag')).eq('ryans-go-bag'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.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 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Misc directories & files 27 | coverage/* 28 | .stylelintcache 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Charcuterie Board with Vue + Vitest 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/vue/components/CharcuterieInventory.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard-scss", 5 | "stylelint-config-recommended-vue" 6 | ], 7 | "rules": { 8 | "at-rule-no-unknown": null, 9 | "scss/at-rule-no-unknown": [ 10 | true, 11 | { 12 | "ignoreAtRules": [ 13 | "screen", 14 | "extends", 15 | "responsive", 16 | "tailwind" 17 | ] 18 | } 19 | ], 20 | "block-no-empty": null, 21 | "max-line-length": null 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "vue-eslint-parser", 4 | "parserOptions": { 5 | "parser": "@typescript-eslint/parser", 6 | "ecmaVersion": 2020, 7 | "sourceType": "module" 8 | }, 9 | "rules": { 10 | "no-undef": "off", 11 | "@typescript-eslint/ban-ts-comment": "off" 12 | }, 13 | "env": { 14 | "browser": true, 15 | "amd": true, 16 | "node": true 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "extends": [ 22 | "eslint:recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:vue/recommended" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "paths": { 16 | "@/*": [ 17 | "./src/*" 18 | ] 19 | } 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /src/vue/components/CharcuterieBoardLogo.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | color: rgba(255 255 255 / 87%); 7 | background-color: rgba(97 65 16 / 67%); 8 | font-synthesis: none; 9 | text-rendering: optimizelegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | a { 15 | font-weight: 500; 16 | color: #646cff; 17 | text-decoration: inherit; 18 | } 19 | 20 | a:hover { 21 | color: #535bf2; 22 | } 23 | 24 | body { 25 | margin: 0; 26 | display: flex; 27 | min-width: 320px; 28 | min-height: 100vh; 29 | } 30 | 31 | #app { 32 | width: 90%; 33 | margin: 0 auto; 34 | padding: 2rem; 35 | text-align: center; 36 | } 37 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import eslint from 'vite-plugin-eslint'; 5 | import stylelint from 'vite-plugin-stylelint'; 6 | import path from "path"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | eslint({ 13 | fix: true, 14 | }), 15 | stylelint({ 16 | fix: true, 17 | lintInWorker: true 18 | }) 19 | ], 20 | server: { 21 | host: '0.0.0.0', 22 | origin: 'http://localhost:3000', 23 | port: 3000, 24 | strictPort: true, 25 | }, 26 | resolve: { 27 | alias: [ 28 | {find: '@', replacement: path.resolve(__dirname, './src')}, 29 | ], 30 | }, 31 | test: { 32 | globals: true, 33 | environment: "jsdom", 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/__tests__/CharcuterieInventory.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {describe, it, expect} from "vitest"; 3 | import CharcuterieInventory from "@/vue/components/CharcuterieInventory.vue"; 4 | import {kebabCase} from "@/js/utils"; 5 | 6 | describe('CharcuterieInventory.vue', () => { 7 | it('should have
element with the "inventory" class', () => { 8 | const wrapper = mount(CharcuterieInventory, { 9 | }); 10 | const inventory = wrapper.findAll('div.inventory'); 11 | expect(inventory.length).toBe(1); 12 | }); 13 | 14 | it('should have at lease one: , component', () => { 15 | const wrapper = mount(CharcuterieInventory, { 16 | shallow: true 17 | }); 18 | const component = wrapper.find(kebabCase('CharcuterieItem') + '-stub'); 19 | expect(component.exists()).toBe(true); 20 | }); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /src/__tests__/CharcuterieBoard.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from "@vue/test-utils"; 2 | import {describe, it, expect} from "vitest"; 3 | import CharcuterieBoard from "@/vue/components/CharcuterieBoard.vue"; 4 | 5 | describe('CharcuterieBoard.vue', () => { 6 | it('should have
element with the "board" class', () => { 7 | const wrapper = mount(CharcuterieBoard, { 8 | }); 9 | const inventory = wrapper.findAll('div.board'); 10 | expect(inventory.length).toBe(1); 11 | }); 12 | 13 | it('should populate the board with items', () => { 14 | const wrapper = mount(CharcuterieBoard, { 15 | props: { 16 | min: 1, 17 | max: 10, 18 | }, 19 | shallow: true, 20 | }); 21 | wrapper.vm.populateBoard(); 22 | expect(wrapper.vm.items.length).greaterThanOrEqual(1); 23 | expect(wrapper.vm.items.length).lessThanOrEqual(10); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/CharcuterieBoardLogo.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from '@vue/test-utils'; 2 | import {describe, it, expect} from 'vitest'; 3 | import CharcuterieBoardLogo from '@/vue/components/CharcuterieBoardLogo.vue'; 4 | 5 | describe('CharcuterieBoardLogo.vue', () => { 6 | 7 | it('should have an tag', () => { 8 | const wrapper = mount(CharcuterieBoardLogo, { 9 | }); 10 | const a = wrapper.find('a'); 11 | 12 | expect(a.exists()).toBe(true); 13 | expect(a.attributes('href')).toBeDefined(); 14 | }); 15 | 16 | it('should have an tag with the class "logo" & `src` attribute set inside an tag', () => { 17 | const wrapper = mount(CharcuterieBoardLogo, { 18 | }); 19 | const a = wrapper.find('a'); 20 | const img = a.find('img.logo'); 21 | 22 | expect(img.exists()).toBe(true); 23 | expect(img.attributes('src')).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/charcuterie-inventory.test.ts: -------------------------------------------------------------------------------- 1 | import {it, expect, expectTypeOf, describe} from 'vitest'; 2 | import {getInventory, getInventoryItem} from '@/js/charcuterie-inventory'; 3 | 4 | describe('charcuterie-inventory.ts', () => { 5 | it('should return some inventory items', () => { 6 | const items = getInventory(); 7 | expect(items.length).greaterThan(0); 8 | }); 9 | 10 | it('should return an array of nothing but CharcuterieItems', () => { 11 | const items = getInventory(); 12 | items.forEach((item: CharcuterieItem) => { 13 | expectTypeOf(item).toEqualTypeOf(); 14 | }) 15 | }); 16 | 17 | it('should have an "olives" item', () => { 18 | const olives = getInventoryItem('olives'); 19 | expect(typeof olives !== 'undefined').toBe(true); 20 | }); 21 | 22 | it('should have all of the items from our spec', () => { 23 | const inventory = getInventory(); 24 | expect(inventory).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/__tests__/CharcuterieItem.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from '@vue/test-utils'; 2 | import {describe, it, expect} from 'vitest'; 3 | import CharcuterieItem from '@/vue/components/CharcuterieItem.vue'; 4 | import {getInventoryItem} from "@/js/charcuterie-inventory"; 5 | 6 | describe('CharcuterieItem.vue', () => { 7 | 8 | it('should have an tag with the class "icon" & `src` attribute set', () => { 9 | const wrapper = mount(CharcuterieItem, { 10 | props: { 11 | item: getInventoryItem('olives') 12 | }, 13 | }); 14 | const img =wrapper.find('img.icon'); 15 | 16 | expect(img.exists()).toBe(true); 17 | expect(img.attributes('src')).toBeDefined(); 18 | }); 19 | 20 | it('should have a tag with the class "name" & have the correct text in it', () => { 21 | const wrapper = mount(CharcuterieItem, { 22 | props: { 23 | item: getInventoryItem('olives') 24 | }, 25 | }); 26 | const span =wrapper.find('span.name'); 27 | 28 | expect(span.exists()).toBe(true); 29 | expect(span.text()).eq('olives'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/vue/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 46 | -------------------------------------------------------------------------------- /src/vue/components/CharcuterieBoard.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /src/vue/components/CharcuterieItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-app-vitest", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "npm run lint && vite build", 9 | "lint": "npm run lint:ts && npm run lint:js && npm run lint:css", 10 | "lint:ts": "tsc --noEmit", 11 | "lint:js": "eslint './src/**/*.{js,ts,vue}' --fix", 12 | "lint:css": "stylelint './src/**/*.{css,vue}' --fix", 13 | "preview": "vite preview", 14 | "test": "vitest", 15 | "test-coverage": "vitest run --coverage", 16 | "test-ui": "vitest --ui --open=false" 17 | }, 18 | "dependencies": { 19 | "vue": "^3.2.45" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^5.48.0", 23 | "@vitejs/plugin-vue": "^4.0.0", 24 | "@vitest/coverage-c8": "^0.27.0", 25 | "@vitest/ui": "^0.27.0", 26 | "@vue/test-utils": "^2.2.0", 27 | "eslint": "^8.32.0", 28 | "eslint-plugin-vue": "^9.9.0", 29 | "jsdom": "^21.0.0", 30 | "stylelint": "^14.0.0", 31 | "stylelint-config-recommended": "^6.0.0", 32 | "stylelint-config-standard-scss": "^3.0.0", 33 | "stylelint-config-recommended-vue": "^1.0.0", 34 | "typescript": "^4.9.3", 35 | "vite": "^4.0.0", 36 | "vite-plugin-eslint": "^1.8.1", 37 | "vite-plugin-stylelint": "^4.1.0", 38 | "vitest": "^0.27.0", 39 | "vue-tsc": "^1.0.11", 40 | "vue-eslint-parser": "^9.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__tests__/App.test.ts: -------------------------------------------------------------------------------- 1 | import {mount} from '@vue/test-utils'; 2 | import {describe, it, expect} from 'vitest'; 3 | import {kebabCase} from "@/js/utils"; 4 | import App from '@/vue/App.vue'; 5 | 6 | describe('App.vue', () => { 7 | it('should have
element with the "row" class', () => { 8 | const wrapper = mount(App, { 9 | }); 10 | const columns = wrapper.findAll('div.row'); 11 | expect(columns.length).toBe(1); 12 | }); 13 | 14 | it('should have
elements with the "column" class', () => { 15 | const wrapper = mount(App, { 16 | }); 17 | const columns = wrapper.findAll('div.column'); 18 | expect(columns.length).toBe(2); 19 | }); 20 | 21 | it('the first column should have a

with the title "Charcuterie Board"', () => { 22 | const wrapper = mount(App, { 23 | }); 24 | const columns = wrapper.findAll('div.column'); 25 | const header = columns[0].find('h1'); 26 | expect(header.text()).toBe('Charcuterie Board'); 27 | }); 28 | 29 | it('the second column should have a

with the title "Inventory"', () => { 30 | const wrapper = mount(App, { 31 | }); 32 | const columns = wrapper.findAll('div.column'); 33 | const header = columns[1].find('h1'); 34 | expect(header.text()).toBe('Inventory'); 35 | }); 36 | 37 | it('should have the following components: , & ', () => { 38 | const wrapper = mount(App, { 39 | shallow: true 40 | }); 41 | const components = ['CharcuterieBoardLogo', 'CharcuterieBoard', 'CharcuterieInventory']; 42 | components.forEach((name) => { 43 | const component = wrapper.find(kebabCase(name) + '-stub'); 44 | expect(component.exists()).toBe(true); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG?=18-alpine 2 | CONTAINER?=$(shell basename $(CURDIR)) 3 | IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG)) 4 | IMAGE_NAME=${CONTAINER}:${TAG} 5 | DOCKER_RUN=docker container run --rm -it -v `pwd`:/app 6 | 7 | .PHONY: build clean dev image-build image-check npm ssh test test-coverage test-ui 8 | 9 | # Perform a dist build via npm run build 10 | build: image-check 11 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run build 12 | # Remove node_modules/ & package-lock.json 13 | clean: 14 | rm -rf node_modules/ 15 | rm -f package-lock.json 16 | # Run the development server via npm run dev 17 | dev: image-check 18 | ${DOCKER_RUN} --name ${CONTAINER}-$@ -p 3000:3000 ${IMAGE_NAME} run dev 19 | # Build the Docker image & run npm install 20 | image-build: 21 | docker build . -t ${IMAGE_NAME} --build-arg TAG=${TAG} --no-cache 22 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} install 23 | # Ensure the image has been created 24 | image-check: 25 | ifeq ($(IMAGE_INFO), []) 26 | image-check: image-build 27 | endif 28 | # Run the passed in npm command 29 | npm: image-check 30 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS) 31 | # Open a shell inside of the container 32 | ssh: image-check 33 | ${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME} 34 | # Run tests via npm run test 35 | test: image-check 36 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run test 37 | # Run tests with coverage via npm run test-coverage 38 | test-coverage: image-check 39 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run test-coverage 40 | # Run tests with the Vitest UI via npm run test-ui 41 | test-ui: image-check 42 | ${DOCKER_RUN} --name ${CONTAINER}-$@ -p 51204:51204 ${IMAGE_NAME} run test-ui 43 | %: 44 | @: 45 | # ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line 46 | -------------------------------------------------------------------------------- /src/assets/cheese.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/nut.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Charcuterie Vue + Vitest example 2 | 3 | This is the git repo for the project used in the [Test-Driven Development with Vitest](https://craftquest.io/courses/test-driven-development-with-vitest) course on [CraftQuest.io](https://craftquest.io/). The full live stream is here: [CraftQuest on Call 58: TDD with Vitest](https://craftquest.io/livestreams/craftquest-on-call-58-tdd-with-vitest) 4 | 5 | This project uses [Vue 3](https://vuejs.org/), [Vite 4](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [TypeScript](https://www.typescriptlang.org/) to demonstrate test-driven development with Vitest. 6 | 7 | The [`no-code-just-tests`](https://github.com/nystudio107/charcuterie-vue-vitest/tree/no-code-just-tests) branch has just the tests, so you can use TDD to write the code that satisfies them. 8 | 9 | # Requirements 10 | 11 | This project has its devops shrink-wrapped with the project via Docker, and runs inside of a Docker container. You do not need `npm` or `node` installed. 12 | 13 | You'll need only the following installed locally: 14 | 15 | * [Docker Desktop](https://www.docker.com/products/docker-desktop/) 16 | 17 | # Run it in a browser in Github Codespaces 18 | 19 | 1. In [Github](https://github.com/nystudio107/charcuterie-vue-vitest), click on **Use this template** and select **Open in a codespace** 20 | 2. In the resulting Terminal window, type `make dev` to start the project up 21 | 3. Click on the **Open in a Browser** button that appears at the bottom-right 22 | 23 | This lets anyone use the project without having to do _any_ local setup. 24 | 25 | You can use the Codespaces editor to edit files, run tests, or load the site frontend, all from within a browser! 26 | 27 | # Commands 28 | 29 | To use this project, use the following `make` commands from the project root: 30 | 31 | * `make dev` - Run the development server via `npm run dev` 32 | * `make test` - Run tests via `npm run test` 33 | * `make test-coverage` - Run tests with coverage via `npm run test-coverage` 34 | * `make test-ui` - Run tests with the Vitest UI via `npm run test-ui` 35 | * `make build` - Perform a dist build via `npm run build` 36 | * `make npm xxx` - runs the `npm` command passed in, e.g.: `make npm install` 37 | * `make ssh` - Open a shell inside of the container 38 | * `make clean` - Remove node_modules/ & package-lock.json 39 | * `make image-build` - Build the Docker image & run `npm install` 40 | 41 | Port `3000` needs to be available to run the Vite dev server, and port `51204` needs to be available to run the Vitest UI. 42 | 43 | Each command runs in a separate container, so you can use as many of them simultaneously as you want. 44 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/charcuterie-inventory.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`charcuterie-inventory.ts > should have all of the items from our spec 1`] = ` 4 | [ 5 | { 6 | "name": "graham", 7 | "type": "cracker", 8 | }, 9 | { 10 | "name": "cream", 11 | "type": "cracker", 12 | }, 13 | { 14 | "name": "matzo", 15 | "type": "cracker", 16 | }, 17 | { 18 | "name": "graham", 19 | "type": "cracker", 20 | }, 21 | { 22 | "name": "oatcake", 23 | "type": "cracker", 24 | }, 25 | { 26 | "name": "rice", 27 | "type": "cracker", 28 | }, 29 | { 30 | "name": "salami", 31 | "type": "meat", 32 | }, 33 | { 34 | "name": "soppressata", 35 | "type": "meat", 36 | }, 37 | { 38 | "name": "calabrese", 39 | "type": "meat", 40 | }, 41 | { 42 | "name": "mortadella", 43 | "type": "meat", 44 | }, 45 | { 46 | "name": "prosciutto", 47 | "type": "meat", 48 | }, 49 | { 50 | "name": "chorizo", 51 | "type": "meat", 52 | }, 53 | { 54 | "name": "capicola", 55 | "type": "meat", 56 | }, 57 | { 58 | "name": "cheddar", 59 | "type": "cheese", 60 | }, 61 | { 62 | "name": "roquefort", 63 | "type": "cheese", 64 | }, 65 | { 66 | "name": "swiss", 67 | "type": "cheese", 68 | }, 69 | { 70 | "name": "pecorino-romano", 71 | "type": "cheese", 72 | }, 73 | { 74 | "name": "parmigiano-reggiano", 75 | "type": "cheese", 76 | }, 77 | { 78 | "name": "gruyere", 79 | "type": "cheese", 80 | }, 81 | { 82 | "name": "provolone", 83 | "type": "cheese", 84 | }, 85 | { 86 | "name": "peanuts", 87 | "type": "nut", 88 | }, 89 | { 90 | "name": "almond", 91 | "type": "nut", 92 | }, 93 | { 94 | "name": "cashew", 95 | "type": "nut", 96 | }, 97 | { 98 | "name": "walnut", 99 | "type": "nut", 100 | }, 101 | { 102 | "name": "brazil", 103 | "type": "nut", 104 | }, 105 | { 106 | "name": "pistachio", 107 | "type": "nut", 108 | }, 109 | { 110 | "name": "peanuts", 111 | "type": "nut", 112 | }, 113 | { 114 | "name": "apple", 115 | "type": "fruit", 116 | }, 117 | { 118 | "name": "olives", 119 | "type": "fruit", 120 | }, 121 | { 122 | "name": "strawberry", 123 | "type": "fruit", 124 | }, 125 | { 126 | "name": "cherry", 127 | "type": "fruit", 128 | }, 129 | { 130 | "name": "blackberry", 131 | "type": "fruit", 132 | }, 133 | { 134 | "name": "raspberry", 135 | "type": "fruit", 136 | }, 137 | { 138 | "name": "pear", 139 | "type": "fruit", 140 | }, 141 | { 142 | "name": "carrot", 143 | "type": "vegetable", 144 | }, 145 | ] 146 | `; 147 | -------------------------------------------------------------------------------- /src/assets/meat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/js/charcuterie-inventory.ts: -------------------------------------------------------------------------------- 1 | import {CharcuterieItemType} from '@/@types/charcuterie-enums'; 2 | 3 | const charcuterieInventory: CharcuterieItem[] = [ 4 | { 5 | name: 'graham', 6 | type: CharcuterieItemType.Cracker 7 | }, 8 | { 9 | name: 'cream', 10 | type: CharcuterieItemType.Cracker 11 | }, 12 | { 13 | name: 'matzo', 14 | type: CharcuterieItemType.Cracker 15 | }, 16 | { 17 | name: 'graham', 18 | type: CharcuterieItemType.Cracker 19 | }, 20 | { 21 | name: 'oatcake', 22 | type: CharcuterieItemType.Cracker, 23 | }, 24 | { 25 | name: 'rice', 26 | type: CharcuterieItemType.Cracker 27 | }, 28 | { 29 | name: 'salami', 30 | type: CharcuterieItemType.Meat, 31 | }, 32 | { 33 | name: 'soppressata', 34 | type: CharcuterieItemType.Meat, 35 | }, 36 | { 37 | name: 'calabrese', 38 | type: CharcuterieItemType.Meat, 39 | }, 40 | { 41 | name: 'mortadella', 42 | type: CharcuterieItemType.Meat, 43 | }, 44 | { 45 | name: 'prosciutto', 46 | type: CharcuterieItemType.Meat, 47 | }, 48 | { 49 | name: 'chorizo', 50 | type: CharcuterieItemType.Meat, 51 | }, 52 | { 53 | name: 'capicola', 54 | type: CharcuterieItemType.Meat, 55 | }, 56 | { 57 | name: 'cheddar', 58 | type: CharcuterieItemType.Cheese, 59 | }, 60 | { 61 | name: 'roquefort', 62 | type: CharcuterieItemType.Cheese, 63 | }, 64 | { 65 | name: 'swiss', 66 | type: CharcuterieItemType.Cheese, 67 | }, 68 | { 69 | name: 'pecorino-romano', 70 | type: CharcuterieItemType.Cheese, 71 | }, 72 | { 73 | name: 'parmigiano-reggiano', 74 | type: CharcuterieItemType.Cheese, 75 | }, 76 | { 77 | name: 'gruyere', 78 | type: CharcuterieItemType.Cheese, 79 | }, 80 | { 81 | name: 'provolone', 82 | type: CharcuterieItemType.Cheese, 83 | }, 84 | { 85 | name: 'peanuts', 86 | type: CharcuterieItemType.Nut, 87 | }, 88 | { 89 | name: 'almond', 90 | type: CharcuterieItemType.Nut, 91 | }, 92 | { 93 | name: 'cashew', 94 | type: CharcuterieItemType.Nut, 95 | }, 96 | { 97 | name: 'walnut', 98 | type: CharcuterieItemType.Nut, 99 | }, 100 | { 101 | name: 'brazil', 102 | type: CharcuterieItemType.Nut, 103 | }, 104 | { 105 | name: 'pistachio', 106 | type: CharcuterieItemType.Nut, 107 | }, 108 | { 109 | name: 'peanuts', 110 | type: CharcuterieItemType.Nut, 111 | }, 112 | { 113 | name: 'apple', 114 | type: CharcuterieItemType.Fruit, 115 | }, 116 | { 117 | name: 'olives', 118 | type: CharcuterieItemType.Fruit, 119 | }, 120 | { 121 | name: 'strawberry', 122 | type: CharcuterieItemType.Fruit, 123 | }, 124 | { 125 | name: 'cherry', 126 | type: CharcuterieItemType.Fruit, 127 | }, 128 | { 129 | name: 'blackberry', 130 | type: CharcuterieItemType.Fruit, 131 | }, 132 | { 133 | name: 'raspberry', 134 | type: CharcuterieItemType.Fruit, 135 | }, 136 | { 137 | name: 'pear', 138 | type: CharcuterieItemType.Fruit, 139 | }, 140 | { 141 | name: 'carrot', 142 | type: CharcuterieItemType.Vegetable, 143 | }, 144 | ]; 145 | 146 | export function getInventory() { 147 | return charcuterieInventory; 148 | } 149 | 150 | export function getInventoryItem(name: string): CharcuterieItem|undefined { 151 | return charcuterieInventory.find((value: CharcuterieItem) => value.name === name); 152 | } 153 | -------------------------------------------------------------------------------- /src/assets/fruit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/assets/vegetable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 13 | 15 | 16 | 19 | 21 | 22 | 24 | 26 | 29 | 32 | 33 | 35 | 37 | 39 | 41 | 42 | 43 | 44 | 45 | 47 | 49 | 50 | 51 | 53 | 55 | 57 | 59 | 60 | 62 | 64 | 66 | 68 | -------------------------------------------------------------------------------- /src/assets/cracker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 31 | 43 | 62 | 70 | 141 | -------------------------------------------------------------------------------- /src/assets/charcuterie-board-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | 12 | 13 | 14 | Openclipart 15 | 16 | 17 | Cheese Board 18 | 2009-09-24T22:48:36 19 | Cheese Board \n \nblack and white trace \n \nFROM: The Complete Book of Cheese \nby Robert Carlton Brown \nGutenberg EText-No. 14293 20 | http://openclipart.org/detail/27515/cheese-board-by-tom 21 | 22 | 23 | tom 24 | 25 | 26 | 27 | 28 | board 29 | cheese 30 | clip art 31 | clipart 32 | dinner 33 | drink 34 | externalsource 35 | food 36 | grater 37 | image 38 | media 39 | public domain 40 | svg 41 | wine 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | --------------------------------------------------------------------------------