From fa58e665d558eb59526e8328b48e77b3b8fafa5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Thu, 5 Mar 2026 19:32:57 +0800 Subject: [PATCH] Add component tests for presentational components and Login Co-Authored-By: Claude Opus 4.6 --- tests/components/Badge.test.js | 26 ++++++++ tests/components/Button.test.js | 29 +++++++++ tests/components/ButtonFilled.test.js | 29 +++++++++ tests/components/Loading.test.js | 16 +++++ tests/components/Login.test.js | 92 +++++++++++++++++++++++++++ tests/components/Search.test.js | 17 +++++ 6 files changed, 209 insertions(+) create mode 100644 tests/components/Badge.test.js create mode 100644 tests/components/Button.test.js create mode 100644 tests/components/ButtonFilled.test.js create mode 100644 tests/components/Loading.test.js create mode 100644 tests/components/Login.test.js create mode 100644 tests/components/Search.test.js diff --git a/tests/components/Badge.test.js b/tests/components/Badge.test.js new file mode 100644 index 0000000..9c910d5 --- /dev/null +++ b/tests/components/Badge.test.js @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Badge from '@/components/Badge.vue'; + +describe('Badge', () => { + it('renders display text', () => { + const wrapper = mount(Badge, { + props: { isActivated: true, displayText: 'Active' }, + }); + expect(wrapper.text()).toBe('Active'); + }); + + it('has activated class when isActivated is true', () => { + const wrapper = mount(Badge, { + props: { isActivated: true, displayText: 'Active' }, + }); + expect(wrapper.classes()).toContain('badge-activated'); + }); + + it('has deactivated class when isActivated is false', () => { + const wrapper = mount(Badge, { + props: { isActivated: false, displayText: 'Inactive' }, + }); + expect(wrapper.classes()).toContain('badge-deactivated'); + }); +}); diff --git a/tests/components/Button.test.js b/tests/components/Button.test.js new file mode 100644 index 0000000..352b388 --- /dev/null +++ b/tests/components/Button.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Button from '@/components/Button.vue'; + +describe('Button', () => { + it('renders button text', () => { + const wrapper = mount(Button, { + props: { buttonText: 'Click Me' }, + }); + expect(wrapper.text()).toBe('Click Me'); + }); + + it('adds ring classes on mousedown', async () => { + const wrapper = mount(Button, { + props: { buttonText: 'Test' }, + }); + await wrapper.trigger('mousedown'); + expect(wrapper.classes()).toContain('ring'); + }); + + it('removes ring classes on mouseup', async () => { + const wrapper = mount(Button, { + props: { buttonText: 'Test' }, + }); + await wrapper.trigger('mousedown'); + await wrapper.trigger('mouseup'); + expect(wrapper.classes()).not.toContain('ring'); + }); +}); diff --git a/tests/components/ButtonFilled.test.js b/tests/components/ButtonFilled.test.js new file mode 100644 index 0000000..16131b8 --- /dev/null +++ b/tests/components/ButtonFilled.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ButtonFilled from '@/components/ButtonFilled.vue'; + +describe('ButtonFilled', () => { + it('renders button text', () => { + const wrapper = mount(ButtonFilled, { + props: { buttonText: 'Save' }, + }); + expect(wrapper.text()).toBe('Save'); + }); + + it('has filled background class', () => { + const wrapper = mount(ButtonFilled, { + props: { buttonText: 'Save' }, + }); + expect(wrapper.find('button').exists()).toBe(true); + }); + + it('adds ring on mousedown and removes on mouseup', async () => { + const wrapper = mount(ButtonFilled, { + props: { buttonText: 'Test' }, + }); + await wrapper.trigger('mousedown'); + expect(wrapper.classes()).toContain('ring'); + await wrapper.trigger('mouseup'); + expect(wrapper.classes()).not.toContain('ring'); + }); +}); diff --git a/tests/components/Loading.test.js b/tests/components/Loading.test.js new file mode 100644 index 0000000..3c7478f --- /dev/null +++ b/tests/components/Loading.test.js @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Loading from '@/components/Loading.vue'; + +describe('Loading', () => { + it('renders a loader element', () => { + const wrapper = mount(Loading); + expect(wrapper.find('.loader').exists()).toBe(true); + }); + + it('has full-screen overlay classes', () => { + const wrapper = mount(Loading); + expect(wrapper.classes()).toContain('fixed'); + expect(wrapper.classes()).toContain('inset-0'); + }); +}); diff --git a/tests/components/Login.test.js b/tests/components/Login.test.js new file mode 100644 index 0000000..40abab0 --- /dev/null +++ b/tests/components/Login.test.js @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import Login from '@/views/Login/Login.vue'; +import loginStore from '@/stores/login.ts'; + +describe('Login', () => { + let pinia; + + beforeEach(() => { + pinia = createPinia(); + setActivePinia(pinia); + }); + + const mountLogin = (options = {}) => { + return mount(Login, { + global: { + plugins: [pinia], + mocks: { + $route: { + query: {}, + ...options.route, + }, + }, + }, + }); + }; + + it('renders login form', () => { + const wrapper = mountLogin(); + expect(wrapper.find('h2').text()).toBe('LOGIN'); + expect(wrapper.find('#account').exists()).toBe(true); + expect(wrapper.find('#password').exists()).toBe(true); + }); + + it('has disabled login button when fields are empty', () => { + const wrapper = mountLogin(); + const btn = wrapper.find('#login_btn_main_btn'); + expect(btn.attributes('disabled')).toBeDefined(); + }); + + it('enables login button when both fields have values', async () => { + const wrapper = mountLogin(); + const store = loginStore(); + store.auth.username = 'user'; + store.auth.password = 'pass'; + await wrapper.vm.$nextTick(); + const btn = wrapper.find('#login_btn_main_btn'); + expect(btn.attributes('disabled')).toBeUndefined(); + }); + + it('shows error message when isInvalid', async () => { + const wrapper = mountLogin(); + const store = loginStore(); + store.isInvalid = true; + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('Incorrect account or password'); + }); + + it('toggles password visibility', async () => { + const wrapper = mountLogin(); + const store = loginStore(); + store.auth.password = 'secret'; + await wrapper.vm.$nextTick(); + + const pwdInput = wrapper.find('#password'); + expect(pwdInput.attributes('type')).toBe('password'); + + // Click the eye icon to toggle + const eyeToggle = wrapper.find( + 'label[for="passwordt"] span.cursor-pointer', + ); + if (eyeToggle.exists()) { + await eyeToggle.trigger('click'); + await wrapper.vm.$nextTick(); + expect(pwdInput.attributes('type')).toBe('text'); + } + }); + + it('stores return-to URL from route query', () => { + const wrapper = mountLogin({ + route: { query: { 'return-to': 'encodedUrl' } }, + }); + const store = loginStore(); + expect(store.rememberedReturnToUrl).toBe('encodedUrl'); + }); +}); diff --git a/tests/components/Search.test.js b/tests/components/Search.test.js new file mode 100644 index 0000000..8ccad5f --- /dev/null +++ b/tests/components/Search.test.js @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Search from '@/components/Search.vue'; + +describe('Search', () => { + it('renders search form', () => { + const wrapper = mount(Search); + expect(wrapper.find('form[role="search"]').exists()).toBe(true); + }); + + it('contains search input', () => { + const wrapper = mount(Search); + const input = wrapper.find('input[type="search"]'); + expect(input.exists()).toBe(true); + expect(input.attributes('placeholder')).toBe('Search Activity'); + }); +});