{{ partial "publication-authors.html" (dict "authors" .Params.authors) }}
+{{ .Title }}
+{{ .Params.venue }}, {{ .Params.year }} · {{ .Params.publication_type }}
+ {{ with .Params.tags }} +{{ partial "publication-authors.html" (dict "authors" .Params.authors) }}
+{{ .Params.venue }}, {{ .Params.year }} · {{ .Params.publication_type }}
+ {{ with .Params.tags }} +{{ len $pages }} publications · 100 per page
+Search could not load. or browse the pages below.
No publications match this search.
-{{ partial "publication-authors.html" (dict "authors" .Params.authors) }}
-{{ .Params.venue }}, {{ .Params.year }} · {{ .Params.publication_type }}
- {{ with .Params.tags }} -{{ . }}
{{ end }} -Abstract
{{ with .Params.abstract }} {{ $.RenderString (dict "display" "block") . }} + {{ with $.Params.abstract_license_url }} + + {{ end }} {{ else with .Params.external_url }} -Read the abstract and full publication at the source.
+Read the abstract and full publication at the source.
{{ else }}The abstract has not been added yet.
{{ end }} diff --git a/scripts/browser_check.cjs b/scripts/browser_check.cjs index fb5f9f1..8a3d7cf 100644 --- a/scripts/browser_check.cjs +++ b/scripts/browser_check.cjs @@ -89,27 +89,44 @@ let browser; await page.waitForURL('**/publications/'); await page.setViewportSize({ width: 390, height: 844 }); const visible = page.locator('[data-publication-card]:visible'); - const total = await page.locator('[data-publication-card]').count(); + const total = Number(await page.locator('[data-publication-archive]').getAttribute('data-publication-total')); + const pageCount = Math.min(total, 100); + assert.equal(await visible.count(), pageCount); + const waitForSearch = async (target = page) => target.waitForFunction(() => { + const input = document.querySelector('[data-publication-search]'); + const params = new URL(location.href).searchParams; + return (params.get('q') || params.get('author') || '') === input.value.trim() && + /^Showing|^0 publications$/.test(document.querySelector('[data-publication-count]').textContent); + }); + const firstTitle = await visible.first().locator('h2').textContent(); + await page.locator('[data-publication-pagination]').first().getByText('Next', { exact: true }).click(); + await page.waitForURL('**/publications/page/2/'); + assert.equal(await visible.count(), Math.min(100, total - 100)); + assert.notEqual(await visible.first().locator('h2').textContent(), firstTitle); + await page.goto(base + '/publications/'); const search = page.locator('[data-publication-search]'); - await search.fill('Grier 2025'); assert.equal(await visible.count(), 1); - await search.fill('Woisetschlager'); assert(await visible.count() > 0); - await search.fill('Michael Dang’ana'); assert(await visible.count() > 0); - await search.fill('no-result-xyz'); assert.equal(await visible.count(), 0); + await search.fill('Grier 2025'); await waitForSearch(); assert.equal(await visible.count(), 1); + await search.fill('Woisetschlager'); await waitForSearch(); assert(await visible.count() > 0); + await search.fill('Michael Dang’ana'); await waitForSearch(); assert(await visible.count() > 0); + await search.fill('no-result-xyz'); await waitForSearch(); assert.equal(await visible.count(), 0); assert(await page.locator('[data-publication-empty]').isVisible()); - await page.locator('[data-publication-clear]').click(); assert.equal(await visible.count(), total); + await page.locator('[data-publication-clear]').click(); await waitForSearch(); assert.equal(await visible.count(), pageCount); for (const button of await page.locator('[data-publication-tag]').all()) { await button.click(); + await page.waitForFunction(tag => new URL(location.href).searchParams.get('tag') === (tag === 'all' ? null : tag), await button.getAttribute('data-publication-tag')); assert.equal(await button.getAttribute('aria-pressed'), 'true'); assert(await visible.count() > 0, 'Each advertised topic must have publications'); } await page.locator('[data-publication-clear]').click(); await page.locator('[data-publication-research]').selectOption('quantum-computing-systems'); + await page.waitForFunction(() => new URL(location.href).searchParams.get('research') === 'quantum-computing-systems'); const quantumCount = await visible.count(); assert(quantumCount > 0 && quantumCount < total); - await page.reload(); assert.equal(await visible.count(), quantumCount, 'Filters must survive reload'); - await page.goto(base + '/publications/?author=Grier+Jones'); assert(await visible.count() >= 2); - await page.goto(base + '/publications/?tag=invalid&research=invalid'); assert.equal(await visible.count(), total); + await page.reload(); await waitForSearch(); assert.equal(await visible.count(), quantumCount, 'Filters must survive reload'); + await page.goto(base + '/publications/?author=Grier+Jones'); await waitForSearch(); assert(await visible.count() >= 2); + await page.goto(base + '/publications/?tag=invalid&research=invalid'); assert.equal(await visible.count(), pageCount); await page.goto(base + '/research/quantum-computing-systems/'); await page.getByRole('link', { name: 'Browse all publications in this area' }).click(); + await waitForSearch(); assert.equal(await visible.count(), quantumCount); await page.goto(base + '/people/'); assert.equal(await page.locator('.person-card h3 a, .alumni-list li > span:first-child a').count(), 0, 'Member names must be plain text'); @@ -234,8 +251,11 @@ let browser; const plain = await noJS.newPage(); await plain.goto(base + '/'); assert(await plain.locator('#site-nav').isVisible(), 'Navigation needs a no-JS fallback'); await plain.locator('#site-nav a[href="/publications/"]').click(); - assert.equal(await plain.locator('[data-publication-card]:visible').count(), total); + assert.equal(await plain.locator('[data-publication-card]:visible').count(), pageCount); assert.equal(await plain.locator('[data-publication-controls]').isVisible(), false); + await plain.locator('[data-publication-pagination]').first().getByText('Next', { exact: true }).click(); + await plain.waitForURL('**/publications/page/2/'); + assert.equal(await plain.locator('[data-publication-card]:visible').count(), Math.min(100, total - 100)); await noJS.close(); const blockedStorage = await createContext({ viewport: { width: 390, height: 844 } }, false); @@ -247,8 +267,55 @@ let browser; await blocked.goto(base + '/publications/'); await blocked.locator('[data-cookie-accept]').click(); await blocked.locator('[data-publication-search]').fill('Q-DICE'); + await waitForSearch(blocked); assert.equal(await blocked.locator('[data-publication-card]:visible').count(), 1); await blockedStorage.close(); + + // Filtering must search beyond the first page while keeping the DOM bounded. + const largeContext = await createContext({ viewport: { width: 390, height: 844 } }); + const largePage = await largeContext.newPage(); + const seed = JSON.parse(fs.readFileSync(path.join(root, 'publications/index.json'), 'utf8'))[0]; + const largeIndex = Array.from({ length: 10000 }, (_, i) => ({ ...seed, + title: `Stress publication ${i}`, url: seed.url, search: `${seed.search} stress record${i}` })); + let indexLoads = 0; + await largeContext.route('**/publications/index.json', route => { + indexLoads++; + return route.fulfill({ json: largeIndex }); + }); + await largePage.goto(base + '/publications/'); + assert.equal(indexLoads, 0, 'Ordinary browsing should not load the search index'); + await largePage.locator('[data-publication-search]').fill('stress'); + await waitForSearch(largePage); + assert.equal(await largePage.locator('[data-publication-card]').count(), 100); + assert.match(await largePage.locator('[data-publication-count]').textContent(), /of 10000 publications/); + await largePage.locator('[data-publication-pagination]').first().getByText('Next', { exact: true }).click(); + await largePage.waitForFunction(() => document.querySelector('[data-publication-count]').textContent.startsWith('Showing 101')); + assert.equal(await largePage.locator('[data-publication-card]').count(), 100); + await largePage.goBack(); + await largePage.waitForFunction(() => document.querySelector('[data-publication-count]').textContent.startsWith('Showing 1–100')); + const searchStarted = performance.now(); + await largePage.locator('[data-publication-search]').fill('record9999'); + await waitForSearch(largePage); + report.publicationSearch10000Ms = Math.round(performance.now() - searchStarted); + assert.equal(await largePage.locator('[data-publication-card]').count(), 1); + assert.equal(await largePage.locator('[data-publication-card] h2').textContent(), 'Stress publication 9999'); + assert.equal(indexLoads, 1, 'Reuse the index between searches and pages'); + await largeContext.close(); + + const retryContext = await createContext(); + const retryPage = await retryContext.newPage(); + await retryContext.route('**/publications/index.json', route => route.abort()); + await retryPage.goto(base + '/publications/'); + await retryPage.locator('[data-publication-search]').fill('Q-DICE'); + await retryPage.locator('[data-publication-error]').waitFor({ state: 'visible' }); + assert.equal(await retryPage.locator('[data-publication-card]').count(), pageCount); + assert(await retryPage.locator('[data-publication-pagination] a[rel="next"]').first().isVisible()); + await retryContext.unroute('**/publications/index.json'); + await retryPage.locator('[data-publication-retry]').click(); + await waitForSearch(retryPage); + assert.equal(await retryPage.locator('[data-publication-card]').count(), 1); + assert.equal(await retryPage.locator('[data-publication-error]').isVisible(), false); + await retryContext.close(); fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify(report, null, 2)); assert.deepEqual(report.overflow, [], 'Horizontal overflow found'); assert.deepEqual(report.errors, [], 'JavaScript errors found'); diff --git a/tests/test_website_editor.py b/tests/test_website_editor.py index 24e702b..1f52ace 100644 --- a/tests/test_website_editor.py +++ b/tests/test_website_editor.py @@ -2,6 +2,7 @@ import base64 import json +import re from pathlib import Path import shutil import subprocess @@ -159,7 +160,8 @@ def test_saved_forms_build_into_real_pages_and_downloads(self): # Legacy pages can derive their URLs from a title longer than the filename. existing = self.store.get('fledge', 'publications') self.store.save({'kind': 'publications', 'slug': 'fledge', 'revision': existing['revision'], - 'data': {'title': 'An updated Fledge title'}}) + 'data': {'title': 'An updated Fledge title', 'abstract': ''}, + 'body': '\nCompanion material remains available.\n'}) self.assertEqual(self.store.get('fledge', 'publications')['preview'], existing['preview']) self.store.save({'kind': 'personal', 'slug': 'jane-doe', 'data': defaults('personal') | {'layout': 'structured', 'primary_link': {'label': 'A paper', 'url': '/publications/sample-paper/'}, 'highlights': [{'title': 'Editor-created highlight', 'description': 'Works'}]}, 'body': '\n## Editor-created page\nHello.\n'}) @@ -179,9 +181,39 @@ def test_saved_forms_build_into_real_pages_and_downloads(self): self.assertIn('>Additional notes', paper_html) self.assertIn('Companion experiment notes.', paper_html) self.assertNotIn('Read the abstract and full publication', paper_html) - self.assertIn('entanglement', (self.root / 'public/publications/index.html').read_text()) + # Source abstracts must render their formatting, not expose escaped TeX. + survey = self.store.get('a-comprehensive-survey-of-machine-unlearning-techniques-for-large-language-models', 'publications') + survey_html = (self.root / 'public' / survey['preview'].lstrip('/') / 'index.html').read_text() + self.assertIn('LLM unlearning', survey_html) + self.assertNotIn(r'\textit', survey_html) + for slug, equations in { + 'mess-dynamically-learned-inference-time-llm-routing-in-model-zoos-with-service-level-guarantees': 1, + 'building-fault-tolerant-overlays-with-low-node-degrees-for-topic-based-publish-subscribe': 12, + 'how-does-stake-distribution-influence-consensus': 3, + }.items(): + record = self.store.get(slug, 'publications') + markup = (self.root / 'public' / record['preview'].lstrip('/') / 'index.html').read_text() + self.assertEqual(markup.count('