From ab2169031271f5b8dbe24e40abb1dd3ac43483a1 Mon Sep 17 00:00:00 2001 From: Shih-Kun Huang Date: Fri, 22 May 2026 22:59:10 +0800 Subject: [PATCH 1/2] Add category hover navigation --- e2e/accessibility-navigation.spec.js | 3 +- e2e/code-upload.spec.js | 7 +-- e2e/graph-coverage.spec.js | 5 +- e2e/helpers/navigation.js | 37 ++++++++++++++ e2e/logic-coverage.spec.js | 7 +-- e2e/mobile-explorer.spec.js | 3 +- e2e/navigation-state.spec.js | 9 ++-- e2e/path-optimization-metrics.spec.js | 3 +- src/App.css | 69 +++++++++++++++++++++++++-- src/app.js | 55 ++++++++++++++++----- 10 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 e2e/helpers/navigation.js diff --git a/e2e/accessibility-navigation.spec.js b/e2e/accessibility-navigation.spec.js index 8b37f63..8cab5e8 100644 --- a/e2e/accessibility-navigation.spec.js +++ b/e2e/accessibility-navigation.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -18,7 +19,7 @@ test.describe('Accessibility navigation', () => { await page.keyboard.press('Enter'); await expect(page.locator('#app-main')).toBeFocused(); - await page.getByTestId('nav-btn-logic').click(); + await openSectionFromNav(page, 'logic'); await expect(page.getByTestId('section-logic')).toBeFocused(); await expect(page.getByTestId('nav-btn-logic')).toHaveAttribute('aria-current', 'page'); await expect(page.getByTestId('nav-btn-graph')).toHaveAttribute('aria-current', 'false'); diff --git a/e2e/code-upload.spec.js b/e2e/code-upload.spec.js index bdc7695..9756394 100644 --- a/e2e/code-upload.spec.js +++ b/e2e/code-upload.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.describe('Code Upload to CFG', () => { test.beforeEach(async ({ context }) => { @@ -8,7 +9,7 @@ test.describe('Code Upload to CFG', () => { }); test('uploads source code, generates CFG, and shows source mapping', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await page.getByTestId('program-language-select').selectOption('javascript'); await page.getByTestId('code-upload-input').setInputFiles({ @@ -41,7 +42,7 @@ test.describe('Code Upload to CFG', () => { test('keeps mapping correct when switching requirements on complex control flow', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await page.getByTestId('program-language-select').selectOption('javascript'); await page.getByTestId('code-upload-input').setInputFiles({ @@ -88,7 +89,7 @@ test.describe('Code Upload to CFG', () => { test('keeps source mapping consistent when switching node/edge/prime-path criteria', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await page.getByTestId('program-language-select').selectOption('javascript'); await page.getByTestId('code-upload-input').setInputFiles({ diff --git a/e2e/graph-coverage.spec.js b/e2e/graph-coverage.spec.js index 0fdf944..4259289 100644 --- a/e2e/graph-coverage.spec.js +++ b/e2e/graph-coverage.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -9,7 +10,7 @@ test.beforeEach(async ({ context }) => { test.describe('Graph Coverage Browser Tests', () => { test('supports advanced criteria and generated test paths', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await page.getByTestId('criterion-edge-pair').click(); await expect(page.getByTestId('requirement-list')).toContainText('S -> A -> B'); @@ -24,7 +25,7 @@ test.describe('Graph Coverage Browser Tests', () => { test('recomputes requirements and test paths after graph editing', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await page.getByTestId('graph-nodes-input').fill([ 'S,Start,80,170', diff --git a/e2e/helpers/navigation.js b/e2e/helpers/navigation.js new file mode 100644 index 0000000..5092e39 --- /dev/null +++ b/e2e/helpers/navigation.js @@ -0,0 +1,37 @@ +const SECTION_CATEGORY = { + methods: 'foundations', + flow: 'foundations', + types: 'foundations', + codecov: 'foundations', + blackbox: 'input-space', + pbt: 'input-space', + graph: 'graph-model', + mbt: 'graph-model', + slicing: 'graph-model', + logic: 'logic', + groupth: 'logic', + syntax: 'syntax', + symbex: 'generation', + concolic: 'generation', + fuzz: 'generation', + testgen: 'generation', + exploit: 'generation', + sbst: 'generation', + tdd: 'process', + acceptance: 'process', + agile: 'process', + inttest: 'process', + advanced: 'strategy', + rbt: 'strategy', +}; + +export async function openSectionFromNav(page, sectionId) { + if (sectionId === 'all') { + await page.getByTestId('nav-btn-all').click(); + return; + } + const categoryId = SECTION_CATEGORY[sectionId]; + if (!categoryId) throw new Error(`No nav category mapped for section: ${sectionId}`); + await page.getByTestId(`nav-category-${categoryId}`).hover(); + await page.getByTestId(`nav-btn-${sectionId}`).click(); +} diff --git a/e2e/logic-coverage.spec.js b/e2e/logic-coverage.spec.js index be40efd..c3e3a39 100644 --- a/e2e/logic-coverage.spec.js +++ b/e2e/logic-coverage.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -9,7 +10,7 @@ test.beforeEach(async ({ context }) => { test.describe('Logic Coverage Explorer', () => { test('default predicate renders truth table with 8 rows', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-logic').click(); + await openSectionFromNav(page, 'logic'); const explorer = page.getByTestId('logic-coverage'); await expect(explorer).toBeVisible(); @@ -20,7 +21,7 @@ test.describe('Logic Coverage Explorer', () => { test('Implicant Coverage tab shows K-maps with columns=ab, rows=c', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-logic').click(); + await openSectionFromNav(page, 'logic'); await page.getByTestId('logic-criterion-ic').click(); @@ -51,7 +52,7 @@ test.describe('Logic Coverage Explorer', () => { test('switching to a 4-clause predicate yields a 4x4 K-map', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-logic').click(); + await openSectionFromNav(page, 'logic'); await page.getByTestId('logic-example-four-clause').click(); await page.getByTestId('logic-criterion-ic').click(); diff --git a/e2e/mobile-explorer.spec.js b/e2e/mobile-explorer.spec.js index fa45c01..f329605 100644 --- a/e2e/mobile-explorer.spec.js +++ b/e2e/mobile-explorer.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -31,7 +32,7 @@ test.describe('Mobile explorer navigation', () => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto('/index.html'); - await page.getByTestId('nav-btn-fuzz').click(); + await openSectionFromNav(page, 'fuzz'); await expect(page.getByTestId('section-fuzz')).toBeVisible(); await expect(page.getByTestId('fuzz-mobile-nav')).toBeHidden(); }); diff --git a/e2e/navigation-state.spec.js b/e2e/navigation-state.spec.js index e1c9395..9fef38d 100644 --- a/e2e/navigation-state.spec.js +++ b/e2e/navigation-state.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -12,7 +13,7 @@ test.describe('Navigation state', () => { test('restores the last learning section after reload', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-fuzz').click(); + await openSectionFromNav(page, 'fuzz'); await expect(page.getByTestId('section-fuzz')).toBeVisible(); await page.reload(); @@ -24,7 +25,7 @@ test.describe('Navigation state', () => { test('keeps the saved Syntax-Based Testing tab after reload', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-syntax').click(); + await openSectionFromNav(page, 'syntax'); await page.getByRole('tab', { name: /Grammar Coverage|Grammar/ }).click(); await page.reload(); @@ -99,7 +100,7 @@ test.describe('Navigation state', () => { await expect(page.locator('[data-advanced-paper="sailor"]')).toHaveAttribute('aria-selected', 'true'); // Bridge from J3 E2E → TestQuality (an ACH-paper tab). - await page.locator('[data-section="acceptance"]').click(); + await openSectionFromNav(page, 'acceptance'); await page.locator('[data-acceptance-tab="e2ejourney"]').click(); await page.getByTestId('e2e-bridge-tqx').click(); @@ -146,7 +147,7 @@ test.describe('Navigation state', () => { test('does not save the Cloud utility drawer as the active learning section', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-logic').click(); + await openSectionFromNav(page, 'logic'); await page.getByTestId('app-cloud-link').click(); await expect(page.getByTestId('cloud-settings-drawer')).toBeVisible(); diff --git a/e2e/path-optimization-metrics.spec.js b/e2e/path-optimization-metrics.spec.js index 037d9a1..29fbd59 100644 --- a/e2e/path-optimization-metrics.spec.js +++ b/e2e/path-optimization-metrics.spec.js @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { openSectionFromNav } from './helpers/navigation.js'; test.beforeEach(async ({ context }) => { await context.addInitScript(() => { @@ -9,7 +10,7 @@ test.beforeEach(async ({ context }) => { test.describe('Path Optimization Metrics', () => { test('shows before and after path counts', async ({ page }) => { await page.goto('/index.html'); - await page.getByTestId('nav-btn-graph').click(); + await openSectionFromNav(page, 'graph'); await expect(page.getByTestId('test-path-metrics')).toBeVisible(); diff --git a/src/App.css b/src/App.css index 8105347..b4ba207 100644 --- a/src/App.css +++ b/src/App.css @@ -166,14 +166,75 @@ textarea:focus-visible { } .app-nav__buttons { display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 8px; min-width: 0; - overflow-x: auto; - padding: 2px 2px 6px; - scrollbar-width: thin; + overflow: visible; + padding: 2px; +} +.nav-category-menu { + position: relative; + flex: 0 0 auto; +} +.nav-category-menu:hover, +.nav-category-menu:focus-within, +.nav-category-menu.open { + z-index: 130; +} +.nav-category-btn { + padding: 7px 13px; + border: 1px solid var(--app-border); + border-radius: 999px; + background: transparent; + color: var(--app-text-muted); + cursor: pointer; + font: inherit; + font-size: 0.82rem; + font-weight: 800; + white-space: nowrap; + transition: background 0.2s, color 0.2s, border-color 0.15s; +} +.nav-category-menu:hover .nav-category-btn, +.nav-category-menu:focus-within .nav-category-btn, +.nav-category-menu.open .nav-category-btn { + background: rgba(52, 152, 219, 0.1); + border-color: var(--app-primary); + color: var(--app-primary-strong); +} +.nav-category-menu.active .nav-category-btn { + background: var(--app-primary-strong); + border-color: var(--app-primary-strong); + color: #fff; +} +.nav-category-panel { + position: absolute; + top: calc(100% + 8px); + left: 0; + z-index: 120; + min-width: 220px; + display: grid; + gap: 6px; + padding: 10px; + border: 1px solid var(--app-border); + border-radius: var(--app-radius-md); + background: var(--app-surface); + box-shadow: var(--app-shadow-lg); + opacity: 0; + pointer-events: none; + transform: translateY(-4px); + transition: opacity 0.14s, transform 0.14s; +} +.nav-category-menu:hover .nav-category-panel, +.nav-category-menu:focus-within .nav-category-panel, +.nav-category-menu.open .nav-category-panel { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} +.nav-category-panel .nav-btn { + text-align: left; } .nav-btn { flex: 0 0 auto; diff --git a/src/app.js b/src/app.js index 6ddf9bf..e432b6f 100644 --- a/src/app.js +++ b/src/app.js @@ -1338,20 +1338,53 @@ export function renderApp(container) { > ${t('section.all')} - ${SECTION_ORDER.map((sectionId) => ` - - `).join('')} + ${SECTION_TAXONOMY.map((cat) => { + const categoryActive = cat.sectionIds.includes(activeSection); + return ` + `; + }).join('')} `; + nav.querySelectorAll('[data-nav-category-trigger]').forEach((button) => { + const menu = button.closest('[data-nav-category]'); + button.addEventListener('click', (event) => { + event.stopPropagation(); + const nextOpen = !menu.classList.contains('open'); + nav.querySelectorAll('[data-nav-category]').forEach((node) => { + node.classList.remove('open'); + node.querySelector('[data-nav-category-trigger]')?.setAttribute('aria-expanded', 'false'); + }); + menu.classList.toggle('open', nextOpen); + button.setAttribute('aria-expanded', nextOpen ? 'true' : 'false'); + }); + }); + nav.querySelectorAll('[data-section]').forEach((button) => { button.addEventListener('click', () => { setActiveSection(button.dataset.section, true); From d3af1b93267ef73f7de0679b430951608093a080 Mon Sep 17 00:00:00 2001 From: Shih-Kun Huang Date: Fri, 22 May 2026 23:01:28 +0800 Subject: [PATCH 2/2] Update standalone bundle for category nav --- src/standalone.js | 55 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/standalone.js b/src/standalone.js index 3720ed9..03911e2 100644 --- a/src/standalone.js +++ b/src/standalone.js @@ -44200,19 +44200,52 @@ The lattice panel draws the subsumption order \u2014 ACoC \u2192 TWC \u2192 PWC > ${t("section.all")} - ${SECTION_ORDER.map((sectionId) => ` - - `).join("")} + ${SECTION_TAXONOMY.map((cat) => { + const categoryActive = cat.sectionIds.includes(activeSection); + return ` + `; + }).join("")} `; + nav.querySelectorAll("[data-nav-category-trigger]").forEach((button) => { + const menu = button.closest("[data-nav-category]"); + button.addEventListener("click", (event) => { + event.stopPropagation(); + const nextOpen = !menu.classList.contains("open"); + nav.querySelectorAll("[data-nav-category]").forEach((node) => { + var _a3; + node.classList.remove("open"); + (_a3 = node.querySelector("[data-nav-category-trigger]")) == null ? void 0 : _a3.setAttribute("aria-expanded", "false"); + }); + menu.classList.toggle("open", nextOpen); + button.setAttribute("aria-expanded", nextOpen ? "true" : "false"); + }); + }); nav.querySelectorAll("[data-section]").forEach((button) => { button.addEventListener("click", () => { setActiveSection(button.dataset.section, true);