-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
442 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
107
02-basics-2/50-selected-meetup/__tests__/selected-meetup.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
Oops, something went wrong.