refactor(pages,a11y,seo): enhance subpage metadata, semantic HTML, accessibility, and E2E regression tests
- Standardize Next.js Metadata and dynamic generateMetadata across all subpages - Fix duplicate main landmark in publications page by replacing nested main with div - Unify Members page spacing (space-y-10 sm:space-y-14, pt-6 md:pt-8) and responsive H2s - Add aria-label and focus ring to CategoryNav View All links for a11y - Derive dynamic course history semester ranges in Lectures page - Add 3 comprehensive E2E test cases (test_08, test_09, test_10) for highlights, active border, CITE brand links, and English H1 headers
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { semesters } from "../../content/lectures";
|
import { semesters } from "../../content/lectures";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Lectures",
|
||||||
|
description: "AI Agent Networking Lab (ANL) lecture courses and curriculum history",
|
||||||
|
};
|
||||||
|
|
||||||
export default function LecturesPage() {
|
export default function LecturesPage() {
|
||||||
const recentSemesters = semesters.slice(0, 3);
|
const recentSemesters = semesters.slice(0, 3);
|
||||||
const olderSemesters = semesters.slice(3);
|
const olderSemesters = semesters.slice(3);
|
||||||
|
|
||||||
|
const oldestSem = olderSemesters[olderSemesters.length - 1];
|
||||||
|
const newestOlderSem = olderSemesters[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -73,7 +82,7 @@ export default function LecturesPage() {
|
|||||||
Browse all semesters
|
Browse all semesters
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
Course history from 2016 Fall to 2024 Spring
|
Course history from {oldestSem ? `${oldestSem.year} ${oldestSem.term}` : "2004 Spring"} to {newestOlderSem ? `${newestOlderSem.year} ${newestOlderSem.term}` : "2024 Fall"} ({olderSemesters.length} Semesters)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-mono text-xs text-vermillion-dark font-bold border border-vermillion-dark px-3.5 py-1.5 rounded group-open:bg-vermillion-dark group-open:text-white dark:group-open:text-ivory transition-colors">
|
<span className="font-mono text-xs text-vermillion-dark font-bold border border-vermillion-dark px-3.5 py-1.5 rounded group-open:bg-vermillion-dark group-open:text-white dark:group-open:text-ivory transition-colors">
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { activeMembers, alumni } from "../../content/members";
|
import { activeMembers, alumni } from "../../content/members";
|
||||||
import { ProfessorDetailsDialog } from "./_components/ProfessorDetailsDialog";
|
import { ProfessorDetailsDialog } from "./_components/ProfessorDetailsDialog";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Members",
|
||||||
|
description: "AI Agent Networking Lab (ANL) professor, active researchers, and alumni directory",
|
||||||
|
};
|
||||||
|
|
||||||
const DEGREE_LABEL: Record<string, string> = {
|
const DEGREE_LABEL: Record<string, string> = {
|
||||||
Professor: "Professor",
|
Professor: "Professor",
|
||||||
PhD: "Ph. D.",
|
PhD: "Ph. D.",
|
||||||
@@ -24,16 +30,16 @@ export default function MembersPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-content py-8 sm:py-12 space-y-12 sm:space-y-16">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
LAB MEMBERS
|
LAB MEMBERS
|
||||||
</h1>
|
</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Professor Section */}
|
{/* Professor Section */}
|
||||||
<section className="space-y-6 pt-6 md:pt-10 border-t border-line">
|
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
|
||||||
<div className="bg-paper p-8 rounded-lg border border-line grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
<div className="bg-paper p-8 rounded-lg border border-line grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
||||||
<div className="lg:col-span-8 space-y-4">
|
<div className="lg:col-span-8 space-y-4">
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
<h2 className="font-serif text-3xl font-normal text-ink">
|
||||||
@@ -77,10 +83,10 @@ export default function MembersPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Active Researchers Section */}
|
{/* Active Researchers Section */}
|
||||||
<section className="space-y-8 pt-6 md:pt-10 border-t border-line">
|
<section className="space-y-8 pt-6 md:pt-8 border-t border-line">
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-serif text-3xl font-normal text-cobalt-dark">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-cobalt-dark">
|
||||||
Active Researchers
|
Active Researchers
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,10 +154,10 @@ export default function MembersPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Alumni Section (Native <details>/<summary> progressive disclosure) */}
|
{/* Alumni Section (Native <details>/<summary> progressive disclosure) */}
|
||||||
<section className="space-y-6 pt-6 md:pt-10 border-t border-line">
|
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-serif text-3xl font-normal text-ink">
|
<h2 className="font-serif text-2xl sm:text-3xl font-normal text-ink">
|
||||||
Graduates
|
Graduates
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { CategoryNav } from "../_components/CategoryNav";
|
import { CategoryNav } from "../_components/CategoryNav";
|
||||||
import { publications, patents } from "../../../content/publications";
|
import { publications, patents } from "../../../content/publications";
|
||||||
@@ -11,6 +12,17 @@ interface PageProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateMetadata({ params }: PageProps): Metadata {
|
||||||
|
const currentCat = PUB_CATEGORIES.find((c) => c.slug === params.category);
|
||||||
|
if (!currentCat) {
|
||||||
|
return { title: "Publications" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: `${currentCat.label} | Publications`,
|
||||||
|
description: `AI Agent Networking Lab (ANL) ${currentCat.label} research inventory and records`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// SSG static pre-rendering (● SSG) for Next.js 14
|
// SSG static pre-rendering (● SSG) for Next.js 14
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return PUB_CATEGORIES.map((cat) => ({
|
return PUB_CATEGORIES.map((cat) => ({
|
||||||
@@ -84,10 +96,10 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
|||||||
{records.length === 0 ? (
|
{records.length === 0 ? (
|
||||||
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
|
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
|
||||||
<p className="font-serif text-lg text-ink">
|
<p className="font-serif text-lg text-ink">
|
||||||
현재 등록된 실적이 0건입니다.
|
No records found in this category.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-ink-mute font-mono">
|
<p className="text-xs text-ink-mute font-mono">
|
||||||
(D-01 카테고리 정류 확정 후 후속 배치가 수행됩니다)
|
Records will be updated periodically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export function CategoryNav({ currentCategory }: CategoryNavProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/publications/${cat.slug}`}
|
href={`/publications/${cat.slug}`}
|
||||||
className="inline-flex items-center text-xs font-bold text-vermillion-dark hover:underline"
|
aria-label={`View all ${cat.label} records`}
|
||||||
|
className="inline-flex items-center text-xs font-bold text-vermillion-dark hover:underline focus:outline-none focus:ring-1 focus:ring-vermillion-dark rounded"
|
||||||
>
|
>
|
||||||
View All →
|
View All →
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { CategoryNav } from "./_components/CategoryNav";
|
import { CategoryNav } from "./_components/CategoryNav";
|
||||||
import { publications } from "../../content/publications";
|
import { publications } from "../../content/publications";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Publications | ANL",
|
title: "Publications",
|
||||||
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
|
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export default function PublicationsIndexPage() {
|
|||||||
).filter((p): p is (typeof publications)[0] => Boolean(p));
|
).filter((p): p is (typeof publications)[0] => Boolean(p));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<section>
|
<section>
|
||||||
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
|
||||||
@@ -68,13 +69,13 @@ export default function PublicationsIndexPage() {
|
|||||||
)}
|
)}
|
||||||
{pub.venue && (
|
{pub.venue && (
|
||||||
<p className="text-xs italic text-ink-mute">
|
<p className="text-xs italic text-ink-mute">
|
||||||
{pub.venue}
|
{pub.venue}{pub.volume ? `, ${pub.volume}` : ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { standardsBodies } from "../../content/standards";
|
import { standardsBodies } from "../../content/standards";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Standardization",
|
||||||
|
description: "AI Agent Networking Lab (ANL) international standardization research and contributions",
|
||||||
|
};
|
||||||
|
|
||||||
export default function StandardizationPage() {
|
export default function StandardizationPage() {
|
||||||
return (
|
return (
|
||||||
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||||
|
|||||||
@@ -121,5 +121,56 @@ class TestANLHomepageE2E(unittest.TestCase):
|
|||||||
# 4. Assert zero placeholder Funder labels (AC-31)
|
# 4. Assert zero placeholder Funder labels (AC-31)
|
||||||
self.assertNotIn("Funder:", html)
|
self.assertNotIn("Funder:", html)
|
||||||
|
|
||||||
|
def test_08_publications_highlights_and_category_nav(self):
|
||||||
|
"""TC-T1-F8: Verify 5 representative highlights and CategoryNav active border in Publications."""
|
||||||
|
html = self.read_html("publications.html")
|
||||||
|
|
||||||
|
# 1. Verify 5 designated representative publications
|
||||||
|
expected_highlights = [
|
||||||
|
"Globally Integrated Trust Authority (GITA) for Resource-Constrained Edge Devices in IoT and 6G",
|
||||||
|
"Application Level Trust Authority (APPLETA) for Resource-Constrained Edge Devices in IoT and 6G",
|
||||||
|
"mQUIC: Use of QUIC for Handover Support with Connection Migration in Wireless/Mobile Networks",
|
||||||
|
"Use of QUIC for CoAP Transport in IoT Networks",
|
||||||
|
"Use of QUIC for Mobile-Oriented Future Internet (Q-MOFI)",
|
||||||
|
]
|
||||||
|
for title in expected_highlights:
|
||||||
|
self.assertIn(title, html, f"Missing representative highlight publication: '{title}'")
|
||||||
|
|
||||||
|
# 2. Verify Highlights heading & section
|
||||||
|
self.assertIn("Highlights", html)
|
||||||
|
self.assertIn("Categories", html)
|
||||||
|
self.assertIn("(IoT Architecture & Protocols, etc ...)", html)
|
||||||
|
|
||||||
|
# 3. Verify category detail active card border & label
|
||||||
|
patent_html = self.read_html(os.path.join("publications", "patent.html"))
|
||||||
|
self.assertIn("border-2 border-cobalt-dark", patent_html)
|
||||||
|
self.assertIn("Active View ●", patent_html)
|
||||||
|
self.assertIn("Patent", patent_html)
|
||||||
|
|
||||||
|
def test_09_footer_cite_and_brand_links(self):
|
||||||
|
"""TC-T1-F9: Verify Footer CITE abbreviation and brand/contact elements."""
|
||||||
|
html = self.read_html("index.html")
|
||||||
|
|
||||||
|
# 1. Institution link label CITE
|
||||||
|
self.assertIn("<span>CITE</span>", html)
|
||||||
|
self.assertIn("<span>KNU</span>", html)
|
||||||
|
self.assertIn("<span>CSE</span>", html)
|
||||||
|
|
||||||
|
# 2. Advisor contact info
|
||||||
|
self.assertIn("Prof. Seok-Joo Koh", html)
|
||||||
|
self.assertIn("sjkoh@knu.ac.kr", html)
|
||||||
|
|
||||||
|
def test_10_subpage_layout_and_english_headers(self):
|
||||||
|
"""TC-T1-F10: Verify English page headers across all subpages."""
|
||||||
|
subpages = [
|
||||||
|
("members.html", "LAB MEMBERS"),
|
||||||
|
("publications.html", "PUBLICATIONS"),
|
||||||
|
("lectures.html", "LECTURES"),
|
||||||
|
("standardization.html", "International Standardization"),
|
||||||
|
]
|
||||||
|
for rel_path, expected_h1 in subpages:
|
||||||
|
html = self.read_html(rel_path)
|
||||||
|
self.assertIn(expected_h1, html, f"Subpage {rel_path} missing H1 '{expected_h1}'")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user