From db52418fc3ea3b9f397f8f64c7c369ed4e04e120 Mon Sep 17 00:00:00 2001 From: MasterFHC Date: Thu, 24 Sep 2026 15:49:12 +0800 Subject: [PATCH 1/3] fix: keep ongeki search suggestions above song cards --- src/features/ongeki/song-list.css | 6 + .../ongeki-search-suggestions.spec.ts | 114 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/ui-parity/ongeki-search-suggestions.spec.ts diff --git a/src/features/ongeki/song-list.css b/src/features/ongeki/song-list.css index abfa0d2..89da886 100644 --- a/src/features/ongeki/song-list.css +++ b/src/features/ongeki/song-list.css @@ -1,4 +1,10 @@ /* 等价旧版 ongeki-song-list.component.css */ +.ongeki-song-list-page .input-container { + /* Liquefy's backdrop-filter creates a stacking context on both the input + container and cards. Lift the container so its suggestions cover cards. */ + z-index: 3; +} + .ongeki-song-list-page .input-container:focus-within{ color: var(--bs-body-color); background-color: var(--bs-body-bg); diff --git a/tests/ui-parity/ongeki-search-suggestions.spec.ts b/tests/ui-parity/ongeki-search-suggestions.spec.ts new file mode 100644 index 0000000..90cf6e4 --- /dev/null +++ b/tests/ui-parity/ongeki-search-suggestions.spec.ts @@ -0,0 +1,114 @@ +import { expect, test } from '@playwright/test'; + +const REACT_ORIGIN = process.env.REACT_ORIGIN ?? 'https://portal.naominet.live:5173'; +const songs = ['t+pazolite', 'Other artist', 't+pazolite / LeaF'].map((artistName, index) => ({ + id: index + 4, + name: `Search fixture ${index + 1}`, + sortName: `search fixture ${index + 1}`, + artistName, + genre: 'POPS&ANIME', + bossCardId: 0, + bossLevel: 1, + level0: '4,0', + level1: '8,0', + level2: '11,7', + level3: '13,7', + level4: '0,0', +})); + +for (const family of ['liquefy', 'legacy', 'animal-island']) { + for (const color of ['light', 'dark']) { + for (const width of [1280, 390]) { + test(`${family} ${color} ${width}: song suggestions stay above cards and remain clickable`, async ({ + page, + context, + }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + // Keep this regression independent of production accounts and catalogs. + await context.route('**/*', async (route) => { + const url = new URL(route.request().url()); + if (url.origin !== REACT_ORIGIN) return route.abort(); + if (!url.pathname.startsWith('/api/')) return route.continue(); + const data = + url.pathname === '/api/account/status' + ? { banned: false, eulaRequired: false } + : url.pathname === '/api/user/me' + ? { + id: 1, + username: 'fixture', + roles: [], + games: ['ongeki'], + cards: [], + defaultCard: null, + keychips: [], + userTrustKeychips: [], + oauth2s: [], + } + : null; + await route.fulfill({ json: { data, status: { code: 92001 } } }); + }); + await context.addInitScript( + ({ family, color }) => { + localStorage.setItem( + 'currentAccount', + JSON.stringify({ + accessToken: 'fixture-access-token', + refreshToken: 'fixture-refresh-token', + tokenType: 'Bearer', + }), + ); + localStorage.setItem('lang', 'zh'); + localStorage.setItem('themeFamily', family); + localStorage.setItem('colorTheme', color); + localStorage.setItem('dbVersion', '6'); + }, + { family, color }, + ); + + await page.goto(`${REACT_ORIGIN}/ongeki/song`); + await expect(page.locator('.ongeki-song-list-page')).toBeVisible(); + await page.evaluate(async (catalog) => { + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open('Aqua', 6); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + await new Promise((resolve, reject) => { + const transaction = database.transaction('ongekiMusic', 'readwrite'); + for (const song of catalog) transaction.objectStore('ongekiMusic').put(song); + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }); + database.close(); + }, songs); + await page.reload(); + + const content = page.locator('.ongeki-song-list-page'); + const cards = content.locator('.card-btn'); + await expect(cards).toHaveCount(3); + await content.locator('input.form-control-plaintext').fill('t+pazo'); + const suggestions = content.locator('.input-container > .position-absolute'); + await expect(suggestions.locator('a')).toHaveCount(3); + // Visibility alone misses this bug: covered elements are still "visible". + for (const suggestion of await suggestions.locator('a').all()) { + await expect + .poll(() => + suggestion.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return element.contains( + document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2), + ); + }), + ) + .toBe(true); + } + await page.screenshot({ path: testInfo.outputPath('suggestions.png') }); + await suggestions.locator('a').filter({ hasText: 't+pazolite / LeaF' }).click(); + await expect(suggestions).toHaveCount(0); + await expect(cards).toHaveCount(1); + await expect(cards.first()).toContainText('Search fixture 3'); + await expect(content.locator('.input-container .badge')).toContainText('t+pazolite / LeaF'); + }); + } + } +} From 3fee84d50db80ffe392d0590b953633c2d291168 Mon Sep 17 00:00:00 2001 From: MasterFHC Date: Thu, 24 Sep 2026 15:52:25 +0800 Subject: [PATCH 2/3] feat: add latest chunithm song release filters --- src/features/chuni/ChuniV2SongListPage.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/features/chuni/ChuniV2SongListPage.tsx b/src/features/chuni/ChuniV2SongListPage.tsx index db960aa..0e53c7f 100644 --- a/src/features/chuni/ChuniV2SongListPage.tsx +++ b/src/features/chuni/ChuniV2SongListPage.tsx @@ -34,6 +34,9 @@ const RELEASES = [ ['v2 2.20.00', 'LUMINOUS'], ['v2 2.25.00', 'LUMINOUS PLUS'], ['v2 2.30.00', 'VERSE'], + ['v2 2.40.00', 'X-VERSE'], + ['v2 2.45.00', 'X-VERSE-X'], + ['v2 2.50.00', 'MATE'], ] as const; const GENRES = [ From c470423a197ad8f5f79ea07d39a0e8781e3eea0d Mon Sep 17 00:00:00 2001 From: MasterFHC Date: Thu, 24 Sep 2026 16:11:51 +0800 Subject: [PATCH 3/3] chore: keep ongeki regression check local --- .../ongeki-search-suggestions.spec.ts | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 tests/ui-parity/ongeki-search-suggestions.spec.ts diff --git a/tests/ui-parity/ongeki-search-suggestions.spec.ts b/tests/ui-parity/ongeki-search-suggestions.spec.ts deleted file mode 100644 index 90cf6e4..0000000 --- a/tests/ui-parity/ongeki-search-suggestions.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const REACT_ORIGIN = process.env.REACT_ORIGIN ?? 'https://portal.naominet.live:5173'; -const songs = ['t+pazolite', 'Other artist', 't+pazolite / LeaF'].map((artistName, index) => ({ - id: index + 4, - name: `Search fixture ${index + 1}`, - sortName: `search fixture ${index + 1}`, - artistName, - genre: 'POPS&ANIME', - bossCardId: 0, - bossLevel: 1, - level0: '4,0', - level1: '8,0', - level2: '11,7', - level3: '13,7', - level4: '0,0', -})); - -for (const family of ['liquefy', 'legacy', 'animal-island']) { - for (const color of ['light', 'dark']) { - for (const width of [1280, 390]) { - test(`${family} ${color} ${width}: song suggestions stay above cards and remain clickable`, async ({ - page, - context, - }, testInfo) => { - await page.setViewportSize({ width, height: 900 }); - // Keep this regression independent of production accounts and catalogs. - await context.route('**/*', async (route) => { - const url = new URL(route.request().url()); - if (url.origin !== REACT_ORIGIN) return route.abort(); - if (!url.pathname.startsWith('/api/')) return route.continue(); - const data = - url.pathname === '/api/account/status' - ? { banned: false, eulaRequired: false } - : url.pathname === '/api/user/me' - ? { - id: 1, - username: 'fixture', - roles: [], - games: ['ongeki'], - cards: [], - defaultCard: null, - keychips: [], - userTrustKeychips: [], - oauth2s: [], - } - : null; - await route.fulfill({ json: { data, status: { code: 92001 } } }); - }); - await context.addInitScript( - ({ family, color }) => { - localStorage.setItem( - 'currentAccount', - JSON.stringify({ - accessToken: 'fixture-access-token', - refreshToken: 'fixture-refresh-token', - tokenType: 'Bearer', - }), - ); - localStorage.setItem('lang', 'zh'); - localStorage.setItem('themeFamily', family); - localStorage.setItem('colorTheme', color); - localStorage.setItem('dbVersion', '6'); - }, - { family, color }, - ); - - await page.goto(`${REACT_ORIGIN}/ongeki/song`); - await expect(page.locator('.ongeki-song-list-page')).toBeVisible(); - await page.evaluate(async (catalog) => { - const database = await new Promise((resolve, reject) => { - const request = indexedDB.open('Aqua', 6); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - await new Promise((resolve, reject) => { - const transaction = database.transaction('ongekiMusic', 'readwrite'); - for (const song of catalog) transaction.objectStore('ongekiMusic').put(song); - transaction.oncomplete = () => resolve(); - transaction.onerror = () => reject(transaction.error); - }); - database.close(); - }, songs); - await page.reload(); - - const content = page.locator('.ongeki-song-list-page'); - const cards = content.locator('.card-btn'); - await expect(cards).toHaveCount(3); - await content.locator('input.form-control-plaintext').fill('t+pazo'); - const suggestions = content.locator('.input-container > .position-absolute'); - await expect(suggestions.locator('a')).toHaveCount(3); - // Visibility alone misses this bug: covered elements are still "visible". - for (const suggestion of await suggestions.locator('a').all()) { - await expect - .poll(() => - suggestion.evaluate((element) => { - const rect = element.getBoundingClientRect(); - return element.contains( - document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2), - ); - }), - ) - .toBe(true); - } - await page.screenshot({ path: testInfo.outputPath('suggestions.png') }); - await suggestions.locator('a').filter({ hasText: 't+pazolite / LeaF' }).click(); - await expect(suggestions).toHaveCount(0); - await expect(cards).toHaveCount(1); - await expect(cards.first()).toContainText('Search fixture 3'); - await expect(content.locator('.input-container .badge')).toContainText('t+pazolite / LeaF'); - }); - } - } -}