1- import { RouterLinkStub , shallowMount } from "@vue/test-utils" ;
1+ import { RouterLinkStub , mount , shallowMount } from "@vue/test-utils" ;
22import { createPinia } from "pinia" ;
3+ import { nextTick } from "vue" ;
34import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
5+ import TheFooter from "../../src/components/Styling/TheFooter.vue" ;
6+ import ThemeSwitch from "../../src/components/Styling/ThemeSwitch.vue" ;
47
58vi . mock ( "../../src/components/login.ts" , ( ) => ( {
69 session : {
@@ -152,6 +155,11 @@ const componentEntries = Object.entries(componentModules).sort(([a], [b]) =>
152155 a . localeCompare ( b )
153156) ;
154157
158+ const flushPromises = async ( ) => {
159+ await Promise . resolve ( ) ;
160+ await Promise . resolve ( ) ;
161+ } ;
162+
155163function makeProps ( path : string ) : Record < string , unknown > {
156164 if ( path . endsWith ( "/Styling/SharedWith.vue" ) ) {
157165 return {
@@ -176,6 +184,97 @@ function makeProps(path: string): Record<string, unknown> {
176184 return { } ;
177185}
178186
187+ describe ( "Focused Styling Component Tests" , ( ) => {
188+ it ( "ThemeSwitch initializes from saved localStorage theme" , async ( ) => {
189+ localStorage . setItem ( "app-theme" , "light" ) ;
190+
191+ const wrapper = mount ( ThemeSwitch ) ;
192+ await nextTick ( ) ;
193+
194+ const button = wrapper . get ( "button.theme-switch" ) ;
195+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "light" ) ;
196+ expect ( button . attributes ( "aria-checked" ) ) . toBe ( "false" ) ;
197+ expect ( document . documentElement . getAttribute ( "data-theme" ) ) . toBe ( "light" ) ;
198+ } ) ;
199+
200+ it ( "ThemeSwitch defaults to dark and toggles theme on click" , async ( ) => {
201+ const wrapper = mount ( ThemeSwitch ) ;
202+ await nextTick ( ) ;
203+
204+ const button = wrapper . get ( "button.theme-switch" ) ;
205+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "dark" ) ;
206+ expect ( localStorage . getItem ( "app-theme" ) ) . toBe ( "dark" ) ;
207+ expect ( document . documentElement . getAttribute ( "data-theme" ) ) . toBe ( "dark" ) ;
208+
209+ await button . trigger ( "click" ) ;
210+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "light" ) ;
211+ expect ( button . attributes ( "aria-checked" ) ) . toBe ( "false" ) ;
212+ expect ( localStorage . getItem ( "app-theme" ) ) . toBe ( "light" ) ;
213+ expect ( document . documentElement . getAttribute ( "data-theme" ) ) . toBe ( "light" ) ;
214+ } ) ;
215+
216+ it ( "ThemeSwitch toggles theme on keyboard handlers" , async ( ) => {
217+ const wrapper = mount ( ThemeSwitch ) ;
218+ await nextTick ( ) ;
219+
220+ const button = wrapper . get ( "button.theme-switch" ) ;
221+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "dark" ) ;
222+
223+ await button . trigger ( "keydown.enter" ) ;
224+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "light" ) ;
225+
226+ await button . trigger ( "keydown.space" ) ;
227+ expect ( button . attributes ( "data-mode" ) ) . toBe ( "dark" ) ;
228+ } ) ;
229+
230+ it ( "TheFooter renders version metadata and shows last modified date after fetch" , async ( ) => {
231+ const fetchMock = vi . fn ( async ( ) => ( {
232+ json : async ( ) => [
233+ {
234+ commit : { committer : { date : "2026-02-20T10:20:30.000Z" } } ,
235+ } ,
236+ ] ,
237+ } ) ) ;
238+ vi . stubGlobal ( "fetch" , fetchMock as unknown as typeof fetch ) ;
239+
240+ const wrapper = mount ( TheFooter ) ;
241+ await vi . waitFor ( ( ) => {
242+ expect ( wrapper . text ( ) ) . toContain ( "Last Modified: 2026-02-20" ) ;
243+ } ) ;
244+
245+ expect ( wrapper . text ( ) ) . toContain ( "Version: v1.0.0" ) ;
246+ expect ( wrapper . text ( ) ) . toContain ( "web-app-v1.0.0" ) ;
247+ expect ( fetchMock ) . toHaveBeenCalledOnce ( ) ;
248+ } ) ;
249+
250+ it ( "TheFooter keeps last-modified hidden when commit API returns no entries" , async ( ) => {
251+ const fetchMock = vi . fn ( async ( ) => ( {
252+ json : async ( ) => [ ] ,
253+ } ) ) ;
254+ vi . stubGlobal ( "fetch" , fetchMock as unknown as typeof fetch ) ;
255+
256+ const wrapper = mount ( TheFooter ) ;
257+ await flushPromises ( ) ;
258+
259+ expect ( wrapper . text ( ) ) . not . toContain ( "Last Modified:" ) ;
260+ } ) ;
261+
262+ it ( "TheFooter handles fetch failures without crashing" , async ( ) => {
263+ const fetchMock = vi . fn ( async ( ) => {
264+ throw new Error ( "network failure" ) ;
265+ } ) ;
266+ const errorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
267+ vi . stubGlobal ( "fetch" , fetchMock as unknown as typeof fetch ) ;
268+
269+ const wrapper = mount ( TheFooter ) ;
270+ await flushPromises ( ) ;
271+
272+ expect ( wrapper . exists ( ) ) . toBe ( true ) ;
273+ expect ( wrapper . text ( ) ) . not . toContain ( "Last Modified:" ) ;
274+ expect ( errorSpy ) . toHaveBeenCalled ( ) ;
275+ } ) ;
276+ } ) ;
277+
179278describe ( "All Vue Components Smoke Tests" , ( ) => {
180279 beforeEach ( ( ) => {
181280 vi . useFakeTimers ( ) ;
0 commit comments