diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ffa80745..5d14fc65 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,6 @@ "bradlc.vscode-tailwindcss", // Tooling for Tailwind "csstools.postcss", // PostCSS as Tailwind is built on top of it "Vue.volar", // Vue 3 Language support for VSC https://github.com/johnsoncodehk/volar - "ZixuanChen.vitest-explorer" // Vitest extension to run tests inside of VSCode + "vitest.explorer" // Vitest extension to run tests inside of VSCode ] } diff --git a/assets/sprite/svg/book.svg b/assets/sprite/svg/book.svg new file mode 100644 index 00000000..27304272 --- /dev/null +++ b/assets/sprite/svg/book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/sprite/svg/link.svg b/assets/sprite/svg/link.svg new file mode 100644 index 00000000..ecca03ec --- /dev/null +++ b/assets/sprite/svg/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/components.d.ts b/components.d.ts index 0c95ff40..3683fc44 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,6 +7,7 @@ export {} declare module 'vue' { export interface GlobalComponents { + ISvgBook: typeof import('~icons/svg/book')['default'] ISvgCircleCheck: typeof import('~icons/svg/circle-check')['default'] ISvgCircleExclamation: typeof import('~icons/svg/circle-exclamation')['default'] ISvgCircleInfo: typeof import('~icons/svg/circle-info')['default'] @@ -14,6 +15,7 @@ declare module 'vue' { ISvgClose: typeof import('~icons/svg/close')['default'] ISvgEyeHidden: typeof import('~icons/svg/eye-hidden')['default'] ISvgEyeVisible: typeof import('~icons/svg/eye-visible')['default'] + ISvgLink: typeof import('~icons/svg/link')['default'] ISvgMenu: typeof import('~icons/svg/menu')['default'] ISvgSearch: typeof import('~icons/svg/search')['default'] ISvgSpinner: typeof import('~icons/svg/spinner')['default'] diff --git a/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts b/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts new file mode 100644 index 00000000..b041c42f --- /dev/null +++ b/components/blocks/LeaderboardInfo/Desktop/Desktop.test.ts @@ -0,0 +1,36 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { vi } from 'vitest' +import { getByTestId } from 'root/testUtils' +import Desktop from './Desktop.vue' + +describe('', () => { + function getDesktopWrapper() { + return mount(Desktop, { + props: { + leaderboard: { + categories: [{ id: 1, name: 'Any%', slug: 'any' }], + id: 1, + name: 'Stuck in the Train Simulator 2', + slug: 'stuck-in-the-train-sim-2', + }, + todoPlatforms: ['XBox', 'PC'], + }, + }) + } + + afterEach(() => { + vi.restoreAllMocks() + }) + + enableAutoUnmount(afterEach) + + it('should render without crashing', () => { + const wrapper = getDesktopWrapper() + expect(wrapper.isVisible()).toBe(true) + + expect(getByTestId(wrapper, 'title').text()).toBe( + 'Stuck in the Train Simulator 2', + ) + expect(getByTestId(wrapper, 'tag').text()).toBe('Any%') + }) +}) diff --git a/components/blocks/LeaderboardInfo/Desktop/Desktop.vue b/components/blocks/LeaderboardInfo/Desktop/Desktop.vue new file mode 100644 index 00000000..dae18f2d --- /dev/null +++ b/components/blocks/LeaderboardInfo/Desktop/Desktop.vue @@ -0,0 +1,141 @@ + + + + + +elements/buttons/Dropdown/Dropdown.vueelements/buttons/Dropdown/DropdownItem.vue diff --git a/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts b/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts new file mode 100644 index 00000000..10549dcf --- /dev/null +++ b/components/blocks/LeaderboardInfo/LeaderboardInfo.test.ts @@ -0,0 +1,67 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { vi } from 'vitest' +import { getByTestId } from 'root/testUtils' +import LeaderboardInfo from './LeaderboardInfo.vue' + +describe('', () => { + function getLeaderboardInfoWrapper() { + return mount(LeaderboardInfo, { + props: { + leaderboard: { + categories: [], + id: 1, + name: 'Stuck in the Train Simulator 2', + slug: 'stuck-in-the-train-sim-2', + socials: [ + { + icon: 'disc', + name: 'discord', + url: 'https://discord.gg/leaderboards.gg', + }, + { + icon: 'youtube', + name: 'Socials', + url: 'https://youtube.com', + }, + { + icon: 'twit', + name: 'twitter', + url: 'https://twitter.com/bestofdyingtwit', + }, + ], + }, + }, + }) + } + + afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + enableAutoUnmount(afterEach) + + it('should render without crashing', () => { + const wrapper = getLeaderboardInfoWrapper() + expect(wrapper.isVisible()).toBe(true) + }) + + it('should render if device width is large', () => { + vi.stubGlobal('innerWidth', 1980) + const wrapper = getLeaderboardInfoWrapper() + expect(wrapper.html()).toContain('Guides') + }) + + it('should render if device width is small', () => { + vi.stubGlobal('innerWidth', 600) + const wrapper = getLeaderboardInfoWrapper() + expect(wrapper.html()).toContain('Submit Run') + }) + + // TODO: The follow event doesn't trigger in the test, somehow + it.skip('should emit event when the Follow Button is triggered', async () => { + const wrapper = getLeaderboardInfoWrapper() + await getByTestId(wrapper, 'child').trigger('follow') + expect(wrapper.emitted().follow).toBeTruthy() + }) +}) diff --git a/components/blocks/LeaderboardInfo/LeaderboardInfo.vue b/components/blocks/LeaderboardInfo/LeaderboardInfo.vue new file mode 100644 index 00000000..c9395469 --- /dev/null +++ b/components/blocks/LeaderboardInfo/LeaderboardInfo.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts b/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts new file mode 100644 index 00000000..045248ff --- /dev/null +++ b/components/blocks/LeaderboardInfo/Mobile/Mobile.test.ts @@ -0,0 +1,36 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { vi } from 'vitest' +import { getByTestId } from 'root/testUtils' +import Mobile from './Mobile.vue' + +describe('', () => { + function getMobileWrapper() { + return mount(Mobile, { + props: { + leaderboard: { + categories: [{ id: 1, name: 'Any%', slug: 'any' }], + id: 1, + name: 'Stuck in the Train Simulator 2', + slug: 'stuck-in-the-train-sim-2', + }, + todoPlatforms: ['XBox', 'PC'], + }, + }) + } + + afterEach(() => { + vi.restoreAllMocks() + }) + + enableAutoUnmount(afterEach) + + it('should render without crashing', () => { + const wrapper = getMobileWrapper() + expect(wrapper.isVisible()).toBe(true) + + expect(getByTestId(wrapper, 'title').text()).toBe( + 'Stuck in the Train Simulator 2', + ) + expect(getByTestId(wrapper, 'tag').text()).toBe('Any%') + }) +}) diff --git a/components/blocks/LeaderboardInfo/Mobile/Mobile.vue b/components/blocks/LeaderboardInfo/Mobile/Mobile.vue new file mode 100644 index 00000000..99c16642 --- /dev/null +++ b/components/blocks/LeaderboardInfo/Mobile/Mobile.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts new file mode 100644 index 00000000..2ff7c8f8 --- /dev/null +++ b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.test.ts @@ -0,0 +1,23 @@ +import { mount } from '@vue/test-utils' +import PlatformTags from './PlatformTags.vue' + +describe('', () => { + const tags = ['XBox', 'PS4', 'Amiga DS'] + + it('should render without crashing', () => { + const wrapper = mount(PlatformTags, { + props: { + tags, + }, + }) + + expect(wrapper.isVisible()).toBe(true) + + wrapper + .getComponent('div.platform-tags') + .findAllComponents('div') + .forEach((c, i) => { + expect(c.text()).toBe(tags[i]) + }) + }) +}) diff --git a/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue new file mode 100644 index 00000000..ae069c42 --- /dev/null +++ b/components/blocks/LeaderboardInfo/PlatformTags/PlatformTags.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/components/elements/buttons/ButtonLink/ButtonLink.vue b/components/elements/buttons/ButtonLink/ButtonLink.vue index 9e93d234..e06763ac 100644 --- a/components/elements/buttons/ButtonLink/ButtonLink.vue +++ b/components/elements/buttons/ButtonLink/ButtonLink.vue @@ -1,7 +1,12 @@ diff --git a/components/elements/buttons/Dropdown/Dropdown.test.ts b/components/elements/buttons/Dropdown/Dropdown.test.ts new file mode 100644 index 00000000..49a7f567 --- /dev/null +++ b/components/elements/buttons/Dropdown/Dropdown.test.ts @@ -0,0 +1,48 @@ +import { + enableAutoUnmount, + mount, + type ComponentMountingOptions, +} from '@vue/test-utils' +import { getByTestId } from 'root/testUtils' +import Dropdown from './Dropdown.vue' +import DropdownItem from './DropdownItem.vue' + +enableAutoUnmount(afterEach) + +function mountDropdown(options?: ComponentMountingOptions) { + return mount(Dropdown, options) +} + +describe('', () => { + it('should render without crashing', () => { + const wrapper = mountDropdown({ + props: { className: 'test' }, + }) + expect(wrapper.isVisible()).toBe(true) + expect(getByTestId(wrapper, 'toggler').classes()).toContain('test') + }) + + describe('when the toggler is clicked', () => { + it('should render the slot item, then hide it on a second click', async () => { + const itemWrapper = mount(DropdownItem) + + const wrapper = mountDropdown({ + slots: { default: itemWrapper.html() }, + }) + + await getByTestId(wrapper, 'toggler').trigger('click') + expect(wrapper.html()).toContain(itemWrapper.html()) + + await getByTestId(wrapper, 'toggler').trigger('click') + expect(wrapper.html()).not.toContain(itemWrapper.html()) + }) + + it('should apply the style to the dropdown arrow', async () => { + const wrapper = mountDropdown() + + await getByTestId(wrapper, 'toggler').trigger('click') + + expect(getByTestId(wrapper, 'arrow').classes()).toContain('isOpen') + }) + }) +}) diff --git a/components/elements/buttons/Dropdown/Dropdown.vue b/components/elements/buttons/Dropdown/Dropdown.vue new file mode 100644 index 00000000..c00aa1f9 --- /dev/null +++ b/components/elements/buttons/Dropdown/Dropdown.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/components/elements/buttons/Dropdown/DropdownItem.test.ts b/components/elements/buttons/Dropdown/DropdownItem.test.ts new file mode 100644 index 00000000..2092e148 --- /dev/null +++ b/components/elements/buttons/Dropdown/DropdownItem.test.ts @@ -0,0 +1,29 @@ +import { enableAutoUnmount, shallowMount } from '@vue/test-utils' +import { getByTestId } from 'root/testUtils' +import DropdownItem from './DropdownItem.vue' +import type { ComponentMountingOptions } from '@vue/test-utils' + +enableAutoUnmount(afterEach) + +function mountDropdownItem( + options?: ComponentMountingOptions, +) { + return shallowMount(DropdownItem, options) +} + +describe('', () => { + it('should render without crashing', () => { + const wrapper = mountDropdownItem({ + props: { slots: 'test' }, + }) + expect(wrapper.isVisible()).toBe(true) + }) + + it("should trigger the click event when it's, well, clicked on", async () => { + const wrapper = mountDropdownItem({}) + + await getByTestId(wrapper, 'container').trigger('click') + + expect(wrapper.emitted().click).toBeTruthy() + }) +}) diff --git a/components/elements/buttons/Dropdown/DropdownItem.vue b/components/elements/buttons/Dropdown/DropdownItem.vue new file mode 100644 index 00000000..247ec19a --- /dev/null +++ b/components/elements/buttons/Dropdown/DropdownItem.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/components/elements/buttons/FollowButton/FollowButton.vue b/components/elements/buttons/FollowButton/FollowButton.vue index eed25169..9bfda74f 100644 --- a/components/elements/buttons/FollowButton/FollowButton.vue +++ b/components/elements/buttons/FollowButton/FollowButton.vue @@ -1,19 +1,15 @@