rename use-case to modules

This commit is contained in:
Julien Calixte
2023-04-09 12:34:43 +02:00
parent 5719fabac1
commit cd49c4cf0a
20 changed files with 17 additions and 17 deletions

View File

@@ -0,0 +1,12 @@
import { withPlugins } from '@/tests/utils'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import NewTaskVue from './NewTaskForm.vue'
describe('New Task Form', () => {
it('displays New Task Form title', () => {
const wrapper = mount(NewTaskVue, withPlugins())
expect(wrapper.text()).toContain('Create a task')
})
})

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { createUuid } from '@/shared/create-uuid'
import { faker } from '@faker-js/faker'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { createStepFixture } from '../models/step.fixture'
import { Task } from '../models/task'
import { useTaskStore } from '../stores/useTask.store'
import StepInput from './StepInput.vue'
const store = useTaskStore()
const router = useRouter()
const id = createUuid()
const title = ref(faker.animal.bird())
const steps = ref([createStepFixture(), createStepFixture()])
const totalEstimation = computed(() =>
steps.value.map((step) => step.estimation).reduce((a, b) => a + b, 0)
)
const saveTask = () => {
const task = new Task(id, title.value)
task.addSteps(...steps.value)
if (Task.validate(task)) {
store.saveTask(task)
router.push({
name: 'home'
})
}
return false
}
</script>
<template>
<div>
<h1>Create a task</h1>
<h2>Estimation: {{ totalEstimation }} minutes</h2>
<form @submit.prevent="saveTask">
<button type="submit">save task</button>
<div>
<label for="title">Title</label>
<input type="text" id="title" v-model="title" />
</div>
<StepInput v-model="steps" />
</form>
</div>
</template>
<style scoped lang="scss">
form {
padding: 1rem 0;
}
</style>

View File

@@ -0,0 +1,38 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { createStepFixture } from '../models/step.fixture'
import StepInput from './StepInput.vue'
describe('Step input textarea', () => {
it('displays a text area with steps inside', () => {
const wrapper = mount(StepInput, {
props: {
modelValue: []
}
})
expect(wrapper.get('textarea')).toBeDefined()
})
it('displays the steps in the textarea', () => {
const steps = [
createStepFixture(),
createStepFixture(),
createStepFixture()
]
const stepsInTextarea = steps
.map((s) => `- ${s.title} | ${s.estimation}`)
.join('\n')
const wrapper = mount(StepInput, {
props: {
modelValue: steps
}
})
const textarea = wrapper.get('textarea')
expect(textarea.element.value).toEqual(stepsInTextarea)
})
})

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
adaptStepsToTextarea,
adaptTextareaToSteps
} from '../infra/adaptStepsToTextarea'
import type { Step } from '../models/step'
const props = defineProps<{
modelValue: Step[]
}>()
const emit = defineEmits<{
(event: 'update:modelValue', payload: Step[]): void
}>()
const rawSteps = ref(adaptStepsToTextarea(props.modelValue))
const stepsTextarea = computed({
get() {
return rawSteps.value
},
set(value) {
rawSteps.value = value
emit('update:modelValue', adaptTextareaToSteps(value))
}
})
</script>
<template>
<div class="step-input">
<label for="steps">steps</label>
<textarea
id="steps"
name="steps"
v-model="stepsTextarea"
cols="40"
rows="20"
></textarea>
</div>
</template>
<style scoped>
.step-input {
}
</style>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { useTaskStore } from '@/modules/task/stores/useTask.store'
import { formatDate } from '@/shared/format-date'
const taskStore = useTaskStore()
</script>
<template>
<ul class="task-list">
<li v-for="task in taskStore.recentTasks" :key="task.id">
<router-link :to="{ name: 'task-view', params: { id: task.id } }">{{
task.title
}}</router-link>
| {{ task.totalEstimation }} minutes |
{{ formatDate(task.date) }}
</li>
</ul>
</template>
<style scoped>
.task-list {
}
</style>

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest'
import { createStepFixture } from '../models/step.fixture'
import {
adaptStepsToTextarea,
adaptTextareaToSteps
} from './adaptStepsToTextarea'
describe('adapt steps to textarea value', () => {
it('returns a string with the listed steps', () => {
const steps = [
createStepFixture(),
createStepFixture(),
createStepFixture(),
createStepFixture()
]
const stepsInTextarea = steps
.map((step) => `- ${step.title} | ${step.estimation}`)
.join('\n')
expect(adaptStepsToTextarea(steps)).toEqual(stepsInTextarea)
})
it('returns a list of steps from a textarea value', () => {
const stepsInTextarea = `- step 1 | 3
- step 2 | 4
- step 3 | 5`
const expectedSteps = [
createStepFixture({
id: expect.any(String),
title: 'step 1',
estimation: 3
}),
createStepFixture({
id: expect.any(String),
title: 'step 2',
estimation: 4
}),
createStepFixture({
id: expect.any(String),
title: 'step 3',
estimation: 5
})
]
expect(adaptTextareaToSteps(stepsInTextarea)).toEqual(expectedSteps)
})
it('fallbacks to 0 for the estimation if there is no estimation', () => {
const stepInTextarea = '- step 1'
const expectedStep = createStepFixture({
id: expect.any(String),
title: 'step 1',
estimation: 0
})
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
})
it('fallbacks to 0 for the estimation if it can not read the number', () => {
const stepInTextarea = '- step 1 | not an estimation'
const expectedStep = createStepFixture({
id: expect.any(String),
title: 'step 1',
estimation: 0
})
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
})
it('does not create a step with an empty title', () => {
const stepInTextArea = '\n-'
expect(adaptTextareaToSteps(stepInTextArea)).toEqual([])
})
})

View File

@@ -0,0 +1,38 @@
import { createUuid } from '@/shared/create-uuid'
import { Step } from '../models/step'
export const adaptStepsToTextarea = (steps: Step[]) =>
steps.map((step) => `- ${step.title} | ${step.estimation}`).join('\n')
const extractTitleAndEstimationFromStep = (
rawStep: string
): [string, number] => {
const [rawTitle, rawEstimation] = rawStep
.trim()
.replace(/^-\s*/, '')
.split('|')
const title = rawTitle.trim()
const estimationString = (rawEstimation || '').trim()
const estimation = Number(estimationString)
if (isNaN(estimation)) {
return [title, 0]
}
return [title, estimation]
}
export const adaptTextareaToSteps = (textareaValue: string): Step[] =>
textareaValue
.split('\n')
.map((rawStep) => {
const [title, estimation] = extractTitleAndEstimationFromStep(rawStep)
if (!title) {
return null
}
return new Step(createUuid(), title, estimation)
})
.filter((step) => step !== null) as Step[]

View File

@@ -0,0 +1,12 @@
export enum ResultStatus {
RUN,
DEBRIEF
}
export interface Result {
taskId: string
time: number
steps: Record<string, number>
currentStepId: string | null
status: ResultStatus
}

View File

@@ -0,0 +1,8 @@
export interface Stepable {
id: string
title: string
/**
* estimation in minutes
*/
estimation: number
}

View File

@@ -0,0 +1,9 @@
import type { Stepable } from '@/modules/task/interfaces/stepable'
export interface Taskable {
id: string
title: string
date: Date
link: string | null
steps: Stepable[]
}

View File

@@ -0,0 +1,14 @@
import type { Stepable } from '@/modules/task/interfaces/stepable'
import { Step } from '@/modules/task/models/step'
import { faker } from '@faker-js/faker'
export const createStepFixture = (partialStep?: Partial<Stepable>) =>
new Step(
partialStep?.id ?? faker.datatype.uuid(),
partialStep?.title ?? faker.animal.bird(),
partialStep?.estimation ??
faker.datatype.number({
min: 0,
max: 40
})
)

View File

@@ -0,0 +1,17 @@
import type { Stepable } from '@/modules/task/interfaces/stepable'
export class Step implements Stepable {
constructor(
readonly id: string,
readonly title: string,
readonly estimation: number
) {
return this
}
public static fromStepable(...stepables: Stepable[]): Step[] {
return stepables.map(
(stepable) => new Step(stepable.id, stepable.title, stepable.estimation)
)
}
}

View File

@@ -0,0 +1,14 @@
import { faker } from '@faker-js/faker'
import type { Stepable } from '../interfaces/stepable'
import type { Taskable } from '../interfaces/taskable'
import { createStepFixture } from './step.fixture'
import { Task } from './task'
export const createTaskFixture = (
partialTask?: Partial<Taskable>,
...steps: Stepable[]
) =>
new Task(
partialTask?.id ?? faker.datatype.uuid(),
partialTask?.title ?? faker.animal.bird()
).addSteps(...(steps ?? createStepFixture()))

View File

@@ -0,0 +1,64 @@
import type { Taskable } from '@/modules/task/interfaces/taskable'
import { createStepFixture } from '@/modules/task/models/step.fixture'
import { Task } from '@/modules/task/models/task'
import { faker } from '@faker-js/faker'
import { describe, expect, it } from 'vitest'
describe('Task', () => {
it('has a simple id', () => {
const uuid = faker.datatype.uuid()
const task = new Task(uuid, faker.animal.bear())
expect(task.id).equals(uuid)
})
it('allows a new task from a taskable object', () => {
const taskable: Taskable = {
id: faker.datatype.uuid(),
date: faker.date.recent(),
title: faker.animal.lion(),
link: faker.internet.url(),
steps: [createStepFixture()]
}
const task = Task.fromTaskable(taskable)
expect(task).toEqual(taskable)
})
it('adds steps and removes them', () => {
const task = new Task(faker.datatype.uuid(), faker.color.human())
const [firstStep, secondStep] = [createStepFixture(), createStepFixture()]
task.addSteps(firstStep, secondStep)
expect(task.steps).toEqual([firstStep, secondStep])
task.removeStep(0)
expect(task.steps).toEqual([secondStep])
task.removeStep(0)
expect(task.steps).toEqual([])
})
it('must have an id, a title and steps to be valid', () => {
const task = new Task(faker.datatype.uuid(), faker.color.human())
expect(Task.validate(task)).toEqual(false)
task.addSteps(createStepFixture())
expect(Task.validate(task)).toEqual(true)
})
it('calculates the total estimation of steps', () => {
const task = new Task(faker.datatype.uuid(), faker.color.human())
task.addSteps(
createStepFixture({ estimation: 1 }),
createStepFixture({ estimation: 2 }),
createStepFixture({ estimation: 3 })
)
expect(task.totalEstimation).toEqual(6)
})
})

View File

@@ -0,0 +1,46 @@
import type { Stepable } from '@/modules/task/interfaces/stepable'
import type { Taskable } from '@/modules/task/interfaces/taskable'
import { Step } from '@/modules/task/models/step'
export class Task implements Taskable {
public date = new Date()
public steps: Step[] = []
public link: string | null = null
constructor(public readonly id: string, public readonly title: string) {}
public addSteps(...steps: Stepable[]) {
this.steps.push(...Step.fromStepable(...steps))
return this
}
public removeStep(index: number) {
if (index < 0) {
return this
}
if (index >= this.steps.length) {
return this
}
this.steps.splice(index, 1)
return this
}
public get totalEstimation() {
return this.steps.map((step) => step.estimation).reduce((a, b) => a + b, 0)
}
public static fromTaskable(taskable: Taskable) {
const task = new Task(taskable.id, taskable.title)
task.link = taskable.link
task.date = taskable.date
task.addSteps(...taskable.steps)
return task
}
public static validate(task: Taskable) {
return !!task.id && !!task.title && task.steps.length > 0
}
}

View File

@@ -0,0 +1,42 @@
import { defineStore } from 'pinia'
import type { Taskable } from '../interfaces/taskable'
import { Task } from '../models/task'
interface StoredTaskable extends Omit<Taskable, 'date'> {
date: string
}
export interface TaskStoreState {
tasks: StoredTaskable[]
}
export const useTaskStore = defineStore('task-store', {
persist: true,
state: (): TaskStoreState => ({
tasks: []
}),
actions: {
saveTask(task: Taskable) {
this.tasks.push({
...task,
date: task.date.toISOString()
})
},
reset() {
this.tasks = []
}
},
getters: {
recentTasks(state) {
return state.tasks
.map((task) =>
Task.fromTaskable({ ...task, date: new Date(task.date) })
)
.sort((a, b) => (a.date > b.date ? -1 : 1))
},
getTask() {
return (taskId: string): Task | undefined =>
this.recentTasks.find((task) => task.id === taskId)
}
}
})