Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion e2e/accessibility-navigation.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand All @@ -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');
Expand Down
7 changes: 4 additions & 3 deletions e2e/code-upload.spec.js
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
5 changes: 3 additions & 2 deletions e2e/graph-coverage.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand All @@ -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');
Expand All @@ -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',
Expand Down
37 changes: 37 additions & 0 deletions e2e/helpers/navigation.js
Original file line number Diff line number Diff line change
@@ -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();
}
7 changes: 4 additions & 3 deletions e2e/logic-coverage.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand All @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion e2e/mobile-explorer.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand Down Expand Up @@ -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();
});
Expand Down
9 changes: 5 additions & 4 deletions e2e/navigation-state.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand All @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion e2e/path-optimization-metrics.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
import { openSectionFromNav } from './helpers/navigation.js';

test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
Expand All @@ -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();

Expand Down
69 changes: 65 additions & 4 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
55 changes: 44 additions & 11 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1338,20 +1338,53 @@ export function renderApp(container) {
>
${t('section.all')}
</button>
${SECTION_ORDER.map((sectionId) => `
<button
class="nav-btn${activeSection === sectionId ? ' active' : ''}"
data-testid="nav-btn-${sectionId}"
data-section="${sectionId}"
type="button"
aria-current="${activeSection === sectionId ? 'page' : 'false'}"
>
${t(`section.${sectionId}`)}
</button>
`).join('')}
${SECTION_TAXONOMY.map((cat) => {
const categoryActive = cat.sectionIds.includes(activeSection);
return `
<div class="nav-category-menu${categoryActive ? ' active' : ''}" data-nav-category="${cat.id}">
<button
class="nav-category-btn"
data-testid="nav-category-${cat.id}"
data-nav-category-trigger
type="button"
aria-haspopup="true"
aria-expanded="false"
>
${t(cat.labelKey)}
</button>
<div class="nav-category-panel" role="menu" aria-label="${t(cat.labelKey)}">
${cat.sectionIds.map((sectionId) => `
<button
class="nav-btn${activeSection === sectionId ? ' active' : ''}"
data-testid="nav-btn-${sectionId}"
data-section="${sectionId}"
type="button"
role="menuitem"
aria-current="${activeSection === sectionId ? 'page' : 'false'}"
>
${t(`section.${sectionId}`)}
</button>
`).join('')}
</div>
</div>`;
}).join('')}
</div>
`;

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);
Expand Down
Loading
Loading