From 884b8e7c8ae8b45aa9d2ab7803f623572d65ac02 Mon Sep 17 00:00:00 2001
From: session <252201310+session567@users.noreply.github.com>
Date: Sun, 25 Jan 2026 20:31:52 +0100
Subject: [PATCH] Update week-number module to add dates when player tabs
change on the player detail page
---
src/common/utils/dom.test.ts | 42 ++++++++++++++++++++++-
src/common/utils/dom.ts | 16 +++++++++
src/modules/week-number/index.test.ts | 36 +++++++++++++++++++-
src/modules/week-number/index.ts | 49 ++++++++++++++++++++-------
4 files changed, 128 insertions(+), 15 deletions(-)
diff --git a/src/common/utils/dom.test.ts b/src/common/utils/dom.test.ts
index f4ab21b..c949de3 100644
--- a/src/common/utils/dom.test.ts
+++ b/src/common/utils/dom.test.ts
@@ -1,13 +1,15 @@
+import { createElement } from '@common/test/utils'
import {
getElementById,
getElementsByName,
+ observeElement,
querySelector,
querySelectorAll,
querySelectorAllIn,
querySelectorIn,
} from '@common/utils/dom'
import { logger } from '@common/utils/logger'
-import { beforeEach, describe, expect, it } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
describe(getElementById, () => {
beforeEach(() => {
@@ -207,3 +209,41 @@ describe(querySelectorAllIn, () => {
expect(logger.warn).not.toHaveBeenCalled()
})
})
+
+describe(observeElement, () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+ `
+ })
+
+ it('calls callback when element children change', async () => {
+ const container = document.getElementById('container')!
+ const callback = vi.fn<() => void>()
+
+ observeElement(container, callback)
+
+ container.appendChild(createElement('New content'))
+
+ await vi.waitFor(() => {
+ expect(callback).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ it('calls callback when subtree changes', async () => {
+ const container = document.getElementById('container')!
+ const callback = vi.fn<() => void>()
+
+ observeElement(container, callback)
+
+ // Modify a nested element
+ const content = container.querySelector('.content')!
+ content.textContent = 'Modified content'
+
+ await vi.waitFor(() => {
+ expect(callback).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/src/common/utils/dom.ts b/src/common/utils/dom.ts
index 27cfc5e..a72b27c 100644
--- a/src/common/utils/dom.ts
+++ b/src/common/utils/dom.ts
@@ -117,3 +117,19 @@ export const querySelectorAllIn = (
return elements
}
+
+/**
+ * Observe an element for DOM changes and run a callback when mutations occur.
+ *
+ * @param element - The element to observe for changes
+ * @param callback - Function to execute when the element's children change
+ */
+export const observeElement = (element: Element, callback: () => void) => {
+ const observer = new MutationObserver(() => {
+ observer.disconnect()
+ callback()
+ observer.observe(element, { childList: true, subtree: true })
+ })
+
+ observer.observe(element, { childList: true, subtree: true })
+}
diff --git a/src/modules/week-number/index.test.ts b/src/modules/week-number/index.test.ts
index 6272b60..e54dbab 100644
--- a/src/modules/week-number/index.test.ts
+++ b/src/modules/week-number/index.test.ts
@@ -1,5 +1,15 @@
+import { isPage, pages } from '@common/utils/pages'
import weekNumber from '@modules/week-number'
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock(import('@common/utils/pages'), async () => {
+ const originalModule = await vi.importActual('@common/utils/pages')
+
+ return {
+ ...originalModule,
+ isPage: vi.fn(),
+ }
+})
describe('week-number module', () => {
it.each([
@@ -19,6 +29,30 @@ describe('week-number module', () => {
expect(span?.textContent).toBe(expected)
})
+ it.each([
+ { desc: 'own team', page: pages.playerDetailOwnTeam },
+ { desc: 'other team', page: pages.playerDetailOtherTeam },
+ ])('observes player tabs and adds week numbers on content change', async ({ page }) => {
+ vi.mocked(isPage).mockImplementation((p) => p === page)
+
+ document.body.innerHTML = `
+
+ `
+ weekNumber.run()
+
+ // Simulate content update
+ const playerTabs = document.getElementById('ctl00_ctl00_CPContent_CPMain_updPlayerTabs')!
+ playerTabs.innerHTML = '15.05.2025
'
+
+ await vi.waitFor(() => {
+ const newWeekNumber = playerTabs.querySelector('span.hte-week-number')
+
+ expect(newWeekNumber?.textContent).toBe(' (W3)')
+ })
+ })
+
it("doesn't add week number for invalid date", () => {
document.body.innerHTML = `
diff --git a/src/modules/week-number/index.ts b/src/modules/week-number/index.ts
index 88da593..bc82fe2 100644
--- a/src/modules/week-number/index.ts
+++ b/src/modules/week-number/index.ts
@@ -1,10 +1,32 @@
import '@modules/week-number/index.css'
import type { Module } from '@common/types/module'
-import { querySelectorAll } from '@common/utils/dom'
-import { pages } from '@common/utils/pages'
+import { getElementById, observeElement, querySelectorAllIn } from '@common/utils/dom'
+import { isPage, pages } from '@common/utils/pages'
import { calcWeekNumber, parseDate } from '@modules/week-number/utils'
+/**
+ * Add week numbers to all date elements within the given root element.
+ *
+ * @param root - The parent element to search for date elements within
+ */
+const addWeekNumbers = (root: Element) => {
+ const elements = querySelectorAllIn(root, '.date', false)
+
+ elements.forEach((element) => {
+ const date = parseDate(element)
+ if (!date) return
+
+ const weekNumber = calcWeekNumber(date)
+
+ const span = document.createElement('span')
+ span.className = 'hte-week-number shy'
+ span.textContent = ` (W${weekNumber})`
+
+ element.appendChild(span)
+ })
+}
+
/**
* Display week numbers next to dates throughout Hattrick.
*/
@@ -13,20 +35,21 @@ const weekNumber: Module = {
pages: [pages.all],
excludePages: [pages.forum],
run: () => {
- const elements = querySelectorAll('#mainBody .date', false)
-
- elements.forEach((element) => {
- const date = parseDate(element)
- if (!date) return
+ const mainBody = getElementById('mainBody')
+ if (!mainBody) return
- const weekNumber = calcWeekNumber(date)
+ addWeekNumbers(mainBody)
- const span = document.createElement('span')
- span.className = 'hte-week-number shy'
- span.textContent = ` (W${weekNumber})`
+ // Watch for tab changes on the player detail page. Tab content is loaded asynchronously when clicked, so we need to
+ // re-apply week numbers after each update.
+ if (isPage(pages.playerDetailOwnTeam) || isPage(pages.playerDetailOtherTeam)) {
+ const playerTabs = getElementById('ctl00_ctl00_CPContent_CPMain_updPlayerTabs')
+ if (!playerTabs) return
- element.appendChild(span)
- })
+ observeElement(playerTabs, () => {
+ addWeekNumbers(playerTabs)
+ })
+ }
},
}