Skip to content

Commit

Permalink
Added task Выбранный митап
Browse files Browse the repository at this point in the history
  • Loading branch information
jsru-1 committed Dec 24, 2024
1 parent f827d6c commit be998a1
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 0 deletions.
31 changes: 31 additions & 0 deletions 02-basics-2/50-selected-meetup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Выбранный митап

🔥 _Задача повышенной сложности_\
📚 _Закрепление материала_

<!--start_statement-->

Требуется создать Vue приложение, которое выводит заголовок выбранного митапа:
- На странице отображается заголовок выбранного митапа
- Данные митапа запрашиваются с API функцией `getMeetup`
- Выбор митапа осуществляется радио кнопками с выбором ID от 1 до 5
- Кнопки "Предыдущий" и "Следующий" позволяют соответственно менять выбранный ID
- Кнопки должны быть отключены с `disabled` по достижении крайних значений
- Изначально выбран митап с ID = 1

Требуется минимальное работающее решение. Обрабатывать потенциальную ошибку получения данных или отображать индикацию загрузки не требуется.

<img src="https://i.imgur.com/jSdsjq9.gif" alt="Example">

<!--end_statement-->

---

### Инструкция

📝 Для решения задачи отредактируйте файл: `SelectedMeetupApp.js`.

🚀 Команда запуска для ручного тестирования: `npm run dev`\
Приложение будет доступно на [http://localhost:5173/02-basics-2/50-selected-meetup/](http://localhost:5173/02-basics-2/50-selected-meetup/).

✅ Доступно автоматическое тестирование: `npm test selected-meetup`
16 changes: 16 additions & 0 deletions 02-basics-2/50-selected-meetup/SelectedMeetupApp.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.meetup-selector {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}

.meetup-selector__control {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}

.meetup-selector__cover {
}
78 changes: 78 additions & 0 deletions 02-basics-2/50-selected-meetup/SelectedMeetupApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { defineComponent } from 'vue'
// import { getMeetup } from './meetupsService.ts'

export default defineComponent({
name: 'SelectedMeetupApp',

setup() {},

template: `
<div class="meetup-selector">
<div class="meetup-selector__control">
<button class="button button--secondary" type="button" disabled>Предыдущий</button>
<div class="radio-group" role="radiogroup">
<div class="radio-group__button">
<input
id="meetup-id-1"
class="radio-group__input"
type="radio"
name="meetupId"
value="1"
/>
<label for="meetup-id-1" class="radio-group__label">1</label>
</div>
<div class="radio-group__button">
<input
id="meetup-id-2"
class="radio-group__input"
type="radio"
name="meetupId"
value="2"
/>
<label for="meetup-id-2" class="radio-group__label">2</label>
</div>
<div class="radio-group__button">
<input
id="meetup-id-3"
class="radio-group__input"
type="radio"
name="meetupId"
value="3"
/>
<label for="meetup-id-3" class="radio-group__label">3</label>
</div>
<div class="radio-group__button">
<input
id="meetup-id-4"
class="radio-group__input"
type="radio"
name="meetupId"
value="4"
/>
<label for="meetup-id-4" class="radio-group__label">4</label>
</div>
<div class="radio-group__button">
<input
id="meetup-id-5"
class="radio-group__input"
type="radio"
name="meetupId"
value="5"
/>
<label for="meetup-id-5" class="radio-group__label">5</label>
</div>
</div>
<button class="button button--secondary" type="button">Следующий</button>
</div>
<div class="meetup-selector__cover">
<div class="meetup-cover">
<h1 class="meetup-cover__title">Some Meetup Title</h1>
</div>
</div>
</div>
`,
})
107 changes: 107 additions & 0 deletions 02-basics-2/50-selected-meetup/__tests__/selected-meetup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { MockedFunction } from 'vitest'
import { createWrapperError, flushPromises, mount } from '@vue/test-utils'
import type { VueWrapper, DOMWrapper } from '@vue/test-utils'
import SelectedMeetupApp from '@/SelectedMeetupApp.js'
import { getMeetup } from '@/meetupsService.ts'
import type { MeetupDTO } from '@/meetups.types.ts'

vi.mock('@/meetupsService.ts')

const mockMeetups = {
1: { id: 1, title: 'Meetup 1' },
2: { id: 2, title: 'Meetup Second' },
3: { id: 3, title: 'Meetup III' },
4: { id: 4, title: '4th meetup' },
5: { id: 5, title: 'Meetup 5' },
} as const as unknown as Record<number, MeetupDTO>

const mockGetMeetup = getMeetup as MockedFunction<typeof getMeetup>

mockGetMeetup.mockImplementation((meetupId: number) => Promise.resolve(mockMeetups[meetupId]))

function findByText<T extends HTMLElement>(wrapper: VueWrapper, selector: string, text: string): DOMWrapper<T> {
return wrapper.findAll<T>(selector).find(el => el.text() === text) ?? createWrapperError<DOMWrapper<T>>('DOMWrapper')
}

describe('SelectedMeetupApp', () => {
let wrapper: VueWrapper
let prevButton: DOMWrapper<HTMLButtonElement>
let nextButton: DOMWrapper<HTMLButtonElement>
let meetupIdRadioButtons: DOMWrapper<HTMLInputElement>[]
let meetupIdRadioLabels: DOMWrapper<HTMLLabelElement>[]

beforeEach(() => {
wrapper = mount(SelectedMeetupApp)
prevButton = findByText(wrapper, 'button', 'Предыдущий')
nextButton = findByText(wrapper, 'button', 'Следующий')
meetupIdRadioLabels = wrapper.findAll('[role="radiogroup"] label')
meetupIdRadioButtons = meetupIdRadioLabels.map(label => wrapper.find(`input#${label.attributes('for')}`))
})

it('должно отображать 5 радио кнопок со значениями от 1 до 5, связанных по ID и FOR, а также кнопки "Предыдущий", "Следующий"', () => {
expect(prevButton.exists()).toBe(true)
expect(nextButton.exists()).toBe(true)
expect(meetupIdRadioLabels).toHaveLength(5)
expect(meetupIdRadioButtons).toHaveLength(5)
for (let i = 1; i <= 5; i++) {
expect(meetupIdRadioLabels[i - 1].text()).toBe(i.toString())
expect(meetupIdRadioButtons[i - 1].exists()).toBeTruthy()
}
})

it('должно изначально отображать первый митап выбранным и выводить его заголовок из данных, полученных функцией getMeetup', async () => {
await flushPromises()
expect(meetupIdRadioButtons[0].element.checked).toBeTruthy()
expect(wrapper.text()).toContain('Meetup 1')
})

it('должно отображать кнопку "Предыдущий" отключенной при изначально выбранном первом митапе', async () => {
expect(meetupIdRadioButtons[0].element.checked).toBeTruthy()
expect(prevButton.attributes('disabled')).toBeDefined()
expect(nextButton.attributes('disabled')).not.toBeDefined()
})

it('должно выводить заголовок 2-го митапа выбранным после выбора 2-ой радио кнопки и выводить его заголовок из данных, полученных функцией getMeetup', async () => {
await meetupIdRadioButtons[1].setValue(true)
await flushPromises()
expect(wrapper.text()).toContain('Meetup Second')
})

it('должно выводить заголовок 5-го митапа после выбора 5-й радио кнопки', async () => {
await meetupIdRadioButtons[4].setValue(true)
await flushPromises()
expect(wrapper.text()).toContain('Meetup 5')
})

it('должно отображать кнопку "Следующий" отключенной при выборе последнего митапа', async () => {
await meetupIdRadioButtons[4].setValue(true)
await flushPromises()
expect(prevButton.attributes('disabled')).not.toBeDefined()
expect(nextButton.attributes('disabled')).toBeDefined()
})

it('должно переключать с 3-го на 4-ый митап кнопкой "Следующий", когда был выбран 3-ий, и выводить его заголовок из данных, полученных функцией getMeetup', async () => {
await meetupIdRadioButtons[2].setValue(true)
await flushPromises()
await nextButton.trigger('click')
await flushPromises()
expect(prevButton.attributes('disabled')).not.toBeDefined()
expect(nextButton.attributes('disabled')).not.toBeDefined()
expect(meetupIdRadioButtons[2].element.checked).toBeFalsy()
expect(meetupIdRadioButtons[3].element.checked).toBeTruthy()
expect(wrapper.text()).toContain('4th meetup')
})

it('должно переключать с 4-го на 3-ий митап кнопкой "Предыдущий", когда выбран 4-ый, и выводить его заголовок из данных, полученных функцией getMeetup', async () => {
await meetupIdRadioButtons[3].setValue(true)
await flushPromises()
await prevButton.trigger('click')
await flushPromises()
expect(prevButton.attributes('disabled')).not.toBeDefined()
expect(nextButton.attributes('disabled')).not.toBeDefined()
expect(meetupIdRadioButtons[3].element.checked).toBeFalsy()
expect(meetupIdRadioButtons[2].element.checked).toBeTruthy()
expect(wrapper.text()).toContain('Meetup III')
})
})
16 changes: 16 additions & 0 deletions 02-basics-2/50-selected-meetup/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>selected-meetup</title>
</head>
<body>
<div class="wrapper">
<div class="container">
<h1>Выбранный митап</h1>
<div id="app"></div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions 02-basics-2/50-selected-meetup/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '@shgk/vue-course-ui/meetups/style.css'
import { createApp } from 'vue'
import SelectedMeetupApp from './SelectedMeetupApp.js'
import './SelectedMeetupApp.css'

createApp(SelectedMeetupApp).mount('#app')
Loading

0 comments on commit be998a1

Please sign in to comment.