test work in progress
This commit is contained in:
@@ -11,7 +11,9 @@
|
|||||||
"build:sw": "workbox generateSW workbox-config.js"
|
"build:sw": "workbox generateSW workbox-config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@vee-validate/zod": "^4.12.5",
|
"@vee-validate/zod": "^4.12.5",
|
||||||
|
"@vue/test-utils": "^2.4.4",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@@ -30,12 +32,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vitest/ui": "^1.3.1",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"shadcn-vue": "^0.9.0",
|
"shadcn-vue": "^0.9.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.4",
|
||||||
"vite-plugin-pwa": "^0.19.0",
|
"vite-plugin-pwa": "^0.19.0",
|
||||||
|
"vitest": "^1.3.1",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^1.8.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
714
pnpm-lock.yaml
generated
714
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
21
src/components/EntryForm.test.ts
Normal file
21
src/components/EntryForm.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import EntryForm from './EntryForm.vue'
|
||||||
|
|
||||||
|
describe('Entry Form tests', () => {
|
||||||
|
|
||||||
|
it('takes in values for name and title and displays them in the respected fields', () => {
|
||||||
|
const name = faker.word.noun()
|
||||||
|
const text = faker.lorem.paragraph()
|
||||||
|
const component = mount(EntryForm, {
|
||||||
|
props: {
|
||||||
|
name, text, action: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nameField = component.find('input')
|
||||||
|
const textField = component.find('textarea')
|
||||||
|
console.log(nameField.element.classList, textField.text())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { toTypedSchema } from '@vee-validate/zod'
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Form, FormItem, FormLabel, FormField, FormControl, FormMessage, FormDescription } from '@/components/ui/form';
|
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Entry } from '@/data/entries';
|
import { Entry } from '@/data/entries';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [value: CreateEntrySchema]
|
submit: [value: CreateEntrySchema]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -16,41 +15,38 @@ const props = defineProps<{
|
|||||||
inputEntry?: Entry | undefined
|
inputEntry?: Entry | undefined
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const nameField = ref(props.inputEntry ? props.inputEntry.name : '')
|
||||||
|
const textField = ref(props.inputEntry ? props.inputEntry.text : '')
|
||||||
|
|
||||||
const createEntryZodSchema = z.object({
|
const createEntrySchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
text: z.string().optional()
|
text: z.string().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export type CreateEntrySchema = z.infer<typeof createEntryZodSchema>
|
export type CreateEntrySchema = z.infer<typeof createEntrySchema>
|
||||||
|
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const result = createEntrySchema.safeParse({
|
||||||
|
name: nameField.value,
|
||||||
|
text: textField.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
emit('submit', result.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createEntrySchema = toTypedSchema(createEntryZodSchema)
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Form :validation-schema="createEntrySchema" @submit="(val) => emit('submit', val as CreateEntrySchema)">
|
<form class="flex gap-3 flex-col">
|
||||||
<FormField v-slot="{ componentField }" name="name">
|
<Input placeholder="Name" v-model:model-value="nameField" />
|
||||||
<FormItem>
|
<Textarea placeholder="Text" v-model:model-value="textField" />
|
||||||
<FormControl>
|
<Button class="w-full" v-if="props.action === 'create'" @click="submit()">Create</Button>
|
||||||
<Input placeholder="Name" v-bind="componentField" :model-value="inputEntry ? inputEntry.name : ''" />
|
<Button class="w-full" v-else @click="submit()">Edit</Button>
|
||||||
</FormControl>
|
</form>
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
<FormField v-slot="{ componentField }" name="text">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel></FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder="Text" v-bind="componentField" :model-value="inputEntry ? inputEntry.text : ''" />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
<Button type="submit" class="w-full" v-if="props.action === 'create'">Create</Button>
|
|
||||||
<Button type="submit" class="w-full" v-else>Edit</Button>
|
|
||||||
</Form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
56
src/data/entries.test.ts
Normal file
56
src/data/entries.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { expect, it } from "vitest"
|
||||||
|
import { describe } from "vitest"
|
||||||
|
import { faker } from '@faker-js/faker'
|
||||||
|
import { parseFromPossibleString } from "./entries"
|
||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
|
||||||
|
describe('function for managing entries data entity', () => {
|
||||||
|
function generateList<E>(length: number, generate: () => E) {
|
||||||
|
const arr: Array<E> = []
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
arr.push(generate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateListWithWrongDate() {
|
||||||
|
return generateList(10, () => <any>{
|
||||||
|
name: faker.word.noun(),
|
||||||
|
text: faker.lorem.paragraph(),
|
||||||
|
last_reset: faker.word.sample()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateListWithNoName() {
|
||||||
|
return generateList(10, () => <any>{
|
||||||
|
text: faker.lorem.paragraph(),
|
||||||
|
last_reset: moment()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateListWithCorrectData() {
|
||||||
|
return generateList(10, () => <any>{
|
||||||
|
name: faker.word.noun(),
|
||||||
|
text: faker.lorem.paragraph(),
|
||||||
|
last_reset: moment()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('fails to parse localstorage string because of wrong date', () => {
|
||||||
|
const data = generateListWithWrongDate()
|
||||||
|
expect(parseFromPossibleString(JSON.stringify(data)).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails to parse localstorage string because no name was parsed', () => {
|
||||||
|
const data = generateListWithNoName()
|
||||||
|
expect(parseFromPossibleString(JSON.stringify(data)).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeds to parse localstorage string witht he correct data', () => {
|
||||||
|
const data = generateListWithCorrectData()
|
||||||
|
expect(parseFromPossibleString(JSON.stringify(data)).length).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,39 +1,46 @@
|
|||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { Ref, ref } from "vue";
|
|
||||||
|
|
||||||
const localStorageKey = 'entries'
|
export const localStorageKey = 'entries'
|
||||||
|
|
||||||
export function getDifferenceToToday(date: Moment) {
|
export function getDifferenceToToday(date: Moment) {
|
||||||
return Math.abs(date.diff(moment(), 'days'))
|
return Math.abs(date.diff(moment(), 'days'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const entries: Ref<Entry[]> = ref(parseFromPossibleString(localStorage.getItem(localStorageKey)))
|
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
name: string
|
name: string
|
||||||
text: string | undefined
|
text: string | undefined
|
||||||
last_reset: Moment
|
last_reset: Moment
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFromPossibleString(input: string | null): Entry[] {
|
export function parseFromPossibleString(input: string | null): Entry[] {
|
||||||
if (input === null) {
|
if (input === null) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries: Entry[] = []
|
const entries: Entry[] = []
|
||||||
const rawObjects: any[] = JSON.parse(input)
|
const rawObjects: any[] = JSON.parse(input)
|
||||||
|
|
||||||
for (const rawObject of rawObjects) {
|
for (const rawObject of rawObjects) {
|
||||||
const { name, text, last_reset } = rawObject
|
const { name, text, last_reset } = rawObject
|
||||||
|
const date = parseDate(last_reset)
|
||||||
|
|
||||||
if (name && last_reset) {
|
if (date && name) {
|
||||||
entries.push({ name, text, last_reset: moment(last_reset) })
|
entries.push({ name, text, last_reset: moment(last_reset) })
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return entries
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
export function save() {
|
export function parseDate(dateString: string) {
|
||||||
localStorage.setItem(localStorageKey, JSON.stringify(entries.value))
|
const date = moment(dateString)
|
||||||
|
if (!date.isValid()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save(entries: Entry[]) {
|
||||||
|
localStorage.setItem(localStorageKey, JSON.stringify(entries))
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Card, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { entries, getDifferenceToToday, save } from '@/data/entries';
|
import { entries } from '@/state/entry';
|
||||||
|
import { getDifferenceToToday, save } from '@/data/entries';
|
||||||
import { ArrowBigLeft, MenuIcon, TrashIcon } from 'lucide-vue-next';
|
import { ArrowBigLeft, MenuIcon, TrashIcon } from 'lucide-vue-next';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -40,7 +41,7 @@ function deleteEntry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries.value.splice(index, 1)
|
entries.value.splice(index, 1)
|
||||||
save()
|
save(entries.value)
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ function resetDate() {
|
|||||||
|
|
||||||
entry.value.last_reset = moment()
|
entry.value.last_reset = moment()
|
||||||
confirmDialogState.value = false
|
confirmDialogState.value = false
|
||||||
save()
|
save(entries.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function editEntry(val: CreateEntrySchema) {
|
function editEntry(val: CreateEntrySchema) {
|
||||||
@@ -65,7 +66,7 @@ function editEntry(val: CreateEntrySchema) {
|
|||||||
entry.value.name = val.name
|
entry.value.name = val.name
|
||||||
entry.value.text = val.text
|
entry.value.text = val.text
|
||||||
editEntryDialog.value = false
|
editEntryDialog.value = false
|
||||||
save()
|
save(entries.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Plus } from 'lucide-vue-next';
|
import { Plus } from 'lucide-vue-next';
|
||||||
import { entries, save } from '@/data/entries';
|
import { entries } from '@/state/entry'
|
||||||
|
import { save } from '@/data/entries';
|
||||||
import { Drawer, DrawerHeader, DrawerTitle, DrawerContent } from '@/components/ui/drawer';
|
import { Drawer, DrawerHeader, DrawerTitle, DrawerContent } from '@/components/ui/drawer';
|
||||||
import { Dialog, DialogHeader, DialogTitle, DialogContent } from '@/components/ui/dialog';
|
import { Dialog, DialogHeader, DialogTitle, DialogContent } from '@/components/ui/dialog';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -36,7 +37,7 @@ async function createEntry(value: CreateEntrySchema) {
|
|||||||
? value.text : undefined
|
? value.text : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
save()
|
save(entries.value)
|
||||||
createDrawerState.value = false
|
createDrawerState.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/shims-vue.d.ts
vendored
Normal file
1
src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.vue'
|
||||||
4
src/state/entry.ts
Normal file
4
src/state/entry.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Entry, localStorageKey, parseFromPossibleString } from "@/data/entries";
|
||||||
|
import { Ref, ref } from "vue";
|
||||||
|
|
||||||
|
export const entries: Ref<Entry[]> = ref(parseFromPossibleString(localStorage.getItem(localStorageKey)))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vitest/config'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwind from 'tailwindcss'
|
import tailwind from 'tailwindcss'
|
||||||
@@ -7,6 +7,10 @@ import path from 'path'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true
|
||||||
|
},
|
||||||
plugins: [vue(), VitePWA({
|
plugins: [vue(), VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -27,4 +31,4 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user