@@ -111,7 +114,7 @@
Outcome Breakdown
Computational Reproducibility
-
+
Covers reproductions only: were the original results obtained? Reproduction coverage is still small, so this chart will fill in as more are added.
@@ -124,7 +127,7 @@
Outcome Breakdown
Robustness
-
+
Covers reproductions only: do results hold under alternative analysis choices? Not every reproduction has a robustness check yet.
@@ -137,7 +140,7 @@
Outcome Breakdown
Replicability
-
+
Currently, this distribution covers replications only. Reproductions are shown separately above, since they're coded along different dimensions.
@@ -310,13 +313,13 @@
Recommended Citation
If you use the underlying dataset, please cite it as:
-
+
Loading citation…
If you report results based on this website, please cite it as:
-
+
Wallrich, L., & Röseler, L. (2026). FLoRA Explorer [Website]. https://forrt.org/flora-explorer/
@@ -327,6 +330,7 @@ Recommended Citation
+
Browse Studies
@@ -354,7 +358,7 @@
Outcome Breakdown
Computational Reproducibility
-
+
Reflects the reproductions in the table below: were the original results obtained?
@@ -367,7 +371,7 @@
Outcome Breakdown
Robustness
-
+
Reflects the reproductions in the table below: do results hold under alternative analysis choices?
@@ -380,7 +384,7 @@
Outcome Breakdown
Replicability
-
+
Reflects the studies in the table below. Currently this breakdown covers replications only; reproductions are shown separately when their filter is selected.
@@ -394,13 +398,13 @@
Outcome Breakdown
-
Studies Database
+Studies Database
| + | Study evidence | Original Study | Year | Replication or reproduction study | @@ -424,11 +428,11 @@
|---|
| + | Outcome | Originals | Citations to original | Citations to reproduction | @@ -592,7 +598,7 @@
|---|
| Original | -Year | -Replications | -Citations, all years | -Co-citations since first recorded replication + | + | + | + | Co-citations since first recorded replication | -+ | Timeline |
|---|
About this analysis
+
Mean Citedness
Citation and reuse guidance
@@ -711,7 +718,7 @@Journal Mean Citedness and Recorded Outcomes
OpenAlex Mean Citedness (OMC) of an original study's journal predicts replication success. OMC is a proxy for the Journal Impact Factor. Mean Citedness is a journal-level measure from OpenAlex: the mean citations to its works published in the previous two years. It is retrieved at enrichment time, not matched to the original publication year, and is not the original article’s citation count. The update stamp identifies the analysis snapshot. Any fitted model describes an association. -
+
@@ -743,6 +750,7 @@ Success probability among successful / failed replica
Success probability among successful / failed replica
+ Authorship Overlap
Citation and reuse guidance
@@ -760,7 +768,7 @@ Authorship Overlap and Outcomes
This may produce false positives for common family names when two
unrelated researchers share a surname.
-
+
@@ -794,11 +802,12 @@
+ Publication Type
Citation and reuse guidance
How to cite
Wallrich, L., & Röseler, L. (2026). FLoRA Explorer [Website]. https://forrt.org/flora-explorer/
-
+
@@ -996,13 +1005,13 @@ Outcomes by report size
-
+
+
diff --git a/tests/browser.test.cjs b/tests/browser.test.cjs
index f3091301..f242dca4 100644
--- a/tests/browser.test.cjs
+++ b/tests/browser.test.cjs
@@ -1020,3 +1020,183 @@ test('small-sample and unavailable-data messages use reader-facing words',async(
assert.doesNotMatch(await missing.locator('#kpis').innerText(),/workflow|secrets|GitHub|refresh-data/);
assert.deepEqual(missing.errors,[]);await missing.close();
});
+
+test('keyboard landmarks, citation sorting and paging retain focus and expose state',async()=>{
+ const p=await page('?tab=citations');
+ await p.waitForSelector('#pagination [aria-current="page"]');
+ assert.equal(await p.getByRole('navigation',{name:'Pagination',exact:true}).getAttribute('id'),'pagination');
+ assert.equal(await p.locator('header nav[aria-label="Explorer views"]').count(),1);
+ assert.equal(await p.locator('footer').count(),1);
+ await p.locator('.skip-link').focus();await p.keyboard.press('Enter');
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'main-content');
+ const year=p.locator('#originals-table .table-sort[data-sort="year"]');
+ await year.focus();await p.keyboard.press('Enter');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('#originals-table th[role="button"]').count(),0);
+ await p.keyboard.press('Space');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'ascending');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.sort),'year');
+ await p.getByRole('button',{name:'Sort by co-citation percentage',exact:true}).click();
+ assert.equal(await p.locator('.cocit-th').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('.cocit-sort-btn[data-sort="cocit_prop"]').getAttribute('aria-pressed'),'true');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),null);
+ await p.locator('#pagination [data-action="next"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'2');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.action),'next');
+ await p.locator('#pagination [data-action="previous"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'1');
+ assert.equal(await p.evaluate(()=>document.activeElement.getAttribute('aria-current')),'page');
+ await p.locator('#pagination button[data-p="3"]').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.p),'3');
+ assert.equal(await p.locator('#originals-table th').last().innerText(),'Timeline');
+ for(const size of await p.locator('#originals-table button[data-sort]').evaluateAll(els=>els.map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}))) assert.ok(size.every(v=>v>=24),`sort target ${size}`);
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('mobile paging focuses the results heading; counts and chip changes have one announcement',async()=>{
+ const p=await page('?tab=browse',390);
+ await p.locator('#bm-next').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'browse-results-heading');
+ assert.match(await p.locator('#browse-mobile-meta').innerText(),/Showing 21–40/);
+ assert.equal(await p.locator('#browse-mobile-meta').getAttribute('role'),'status');
+ const position=await p.locator('#browse-results-heading').boundingBox();
+ await p.waitForFunction(()=>document.getElementById('browse-results-heading').getBoundingClientRect().top<150);
+ assert.ok(position);
+ assert.equal(await p.locator('#browse-kind-count[role="status"]').count(),0);
+ await p.selectOption('#mobile-navigation','trends-tab');
+ await p.locator('#trends .trend-filter-btn[data-kind="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#trend-filter-count').getAttribute('role'),'status');
+ assert.match(await p.locator('#trend-filter-count').textContent(),/Numerical Reproductions/);
+ await p.selectOption('#mobile-navigation','mc-tab');
+ await p.waitForSelector('#mc-dist-chart-data');
+ await p.locator('#mc-study-type [data-value="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#analysis-feedback').textContent(),'Mean Citedness study type: Numerical Reproductions selected.');
+ await p.selectOption('#mobile-navigation','overview-tab');
+ const tip=p.locator('#overview .info-icon').first();await tip.focus();
+ assert.equal(await tip.getAttribute('role'),'img');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1');
+ await p.evaluate(()=>document.querySelector('.app-main').scrollTop+=100);
+ await p.waitForFunction(()=>document.querySelector('#overview .info-icon').classList.contains('tip-dismissed'));
+ assert.equal(await tip.locator('.info-tip').evaluate(el=>getComputedStyle(el).pointerEvents),'none');
+ assert.ok(await tip.evaluate(el=>el===document.activeElement));
+ // A touch activation must reopen the definition even when focus has not changed.
+ await tip.dispatchEvent('click');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1',null,{timeout:2000});
+ await p.keyboard.press('Escape');
+ assert.ok(await tip.evaluate(el=>el.classList.contains('tip-dismissed')));
+ await tip.dispatchEvent('focus');
+ assert.ok(await tip.evaluate(el=>!el.classList.contains('tip-dismissed')));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse empty results announce the explanation on desktop and mobile',async()=>{
+ const p=await page('?tab=browse');
+ for(const width of [1280,390]) {
+ await p.setViewportSize({width,height:844});
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ const empty=p.locator('#browse-empty');
+ assert.ok(await empty.isVisible());
+ assert.equal(await empty.getAttribute('role'),'status');
+ assert.match(await empty.innerText(),/No reference pairs match/);
+ await empty.getByRole('button',{name:'Clear search',exact:true}).click();
+ assert.ok(await empty.isHidden());
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse URL kinds and zero-result announcements match the filtered studies',async()=>{
+ for(const [kind,label,expectedKind] of [
+ ['all','All studies','all'],
+ ['replication','Replications','replication'],
+ ['reproduction','Reproductions','reproduction'],
+ ['reproduction-numerical','Numerical Reproductions','reproduction-numerical'],
+ ['reproduction-robustness','Robustness Reproductions','reproduction-robustness'],
+ ['unknown','All studies','all'],
+ ]) {
+ const p=await page('?tab=browse&kind='+kind,390);
+ assert.equal(await p.evaluate(()=>browseKind),expectedKind,kind);
+ assert.equal(await p.locator('#browse-mobile-meta .visually-hidden').textContent(),label+': ',kind);
+ const counts=await p.evaluate(kind=>[getChartData().length,filterByKind(fullRowData,kind).length],expectedKind);
+ assert.ok(counts[0]>0,kind);assert.equal(counts[0],counts[1],kind);
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ assert.equal(await p.locator('#browse-mobile-meta').textContent(),label+': 0 reference pairs',kind);
+ assert.deepEqual(p.errors,[]);await p.close();
+ }
+});
+
+test('chart alternatives and actions have distinct names; motion and forced colours respect preferences',async()=>{
+ const p=await page('?tab=overview',1280,p=>p.emulateMedia({reducedMotion:'reduce'}));
+ assert.equal(await p.evaluate(()=>Chart.getChart('overview-outcome-chart').options.animation),false);
+ assert.match(await p.evaluate(()=>Chart.defaults.font.family),/Source Sans 3/);
+ assert.notEqual(await p.locator('#citation-copy-btn').getAttribute('aria-label'),await p.locator('#website-citation-copy-btn').getAttribute('aria-label'));
+ for(const tab of ['browse','trends','citation','mc','overlap','pub']) {
+ await p.click('#'+tab+'-tab');await p.waitForTimeout(600);
+ assert.equal(await p.locator('.tab-pane.active h1').count(),1,tab);
+ }
+ await p.waitForSelector('#ls-chart-data');
+ for(const selector of ['.chart-copy','.chart-data summary','.chart-data [role="region"]','.chart-mode']) {
+ const names=await p.locator(selector).evaluateAll(els=>els.map(e=>e.getAttribute('aria-label')));
+ assert.ok(names.length>0,`${selector}: no controls found`);
+ assert.ok(names.every(Boolean),selector);assert.equal(new Set(names).size,names.length,`${selector}: ${names}`);
+ }
+ await p.emulateMedia({forcedColors:'active'});
+ const selected=p.locator('#pubtype-study-type .chip.active');
+ assert.equal(await selected.evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ assert.equal(await p.locator('#pub-tab').evaluate(el=>getComputedStyle(el).borderBottomStyle),'double');
+ await p.click('#citation-tab');
+ await p.waitForSelector('.rep-seg');
+ const fills={};
+ for(const outcome of ['successful','failed','mixed']) {
+ // Filtering to an outcome guarantees its segment is present on this page.
+ await p.selectOption('#filter-outcome',outcome);
+ const segment=p.locator('.rep-seg.'+outcome).first();
+ assert.ok(await segment.isVisible(),outcome);
+ fills[outcome]=await segment.evaluate(el=>getComputedStyle(el).backgroundColor+' '+getComputedStyle(el).backgroundImage);
+ }
+ assert.equal(new Set(Object.values(fills)).size,3);
+ assert.match(fills.successful,/repeating-linear-gradient/);
+ assert.ok(await p.locator('.rep-seg').first().evaluate(el=>getComputedStyle(el).forcedColorAdjust==='none'));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('focus rings, small targets and the dark success/failure pair keep sufficient contrast',async()=>{
+ const p=await page('?tab=overview');
+ const linear=v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4;};
+ const rgb=hex=>hex.match(/[a-f\d]{2}/gi).map(x=>parseInt(x,16));
+ const lum=c=>c.map(linear).reduce((n,v,i)=>n+v*[.2126,.7152,.0722][i],0);
+ const contrast=(a,b)=>(Math.max(lum(a),lum(b))+.05)/(Math.min(lum(a),lum(b))+.05);
+ for(const theme of ['light','dark']) {
+ if(await p.evaluate(()=>currentTheme())!==theme) await p.click('#theme-toggle');
+ for(const selector of ['#theme-toggle','.brand-mark','#overview-tab']) {
+ await p.keyboard.press('Tab');await p.locator(selector).focus();
+ assert.equal(await p.locator(selector).evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ const colours=await p.locator(selector).evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(document.querySelector('.top-bar')).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...colours)>=3,`${theme} ${selector}`);
+ }
+ await p.click('#citation-tab');await p.waitForSelector('#pagination button');
+ await p.locator('#search-input').focus();
+ const inputColours=await p.locator('#search-input').evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(el).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...inputColours)>=3);
+ const small=await p.locator('.info-icon, .gloss, footer a, .modal-close, #originals-table button[data-sort]').evaluateAll(els=>els.filter(el=>el.getClientRects().length).map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}));
+ assert.ok(small.every(s=>s.every(v=>v>=24)),JSON.stringify(small));
+ await p.click('#overview-tab');
+ }
+ const colours=await p.evaluate(()=>['--color-chart-successful','--color-chart-failed','--color-surface-raised'].map(t=>FloraCharts.token(t)));
+ const [success,failed,bg]=colours.map(rgb);assert.ok(contrast(success,bg)>=3&&contrast(failed,bg)>=3);
+ // Same full-severity Machado matrices and CIE76 Lab distance used by the review.
+ const lab=([r,g,b])=>{const f=t=>t>.008856?Math.cbrt(t):7.787*t+16/116;return [116*f(.2126*r+.7152*g+.0722*b)-16,500*(f((.4124*r+.3576*g+.1805*b)/.95047)-f(.2126*r+.7152*g+.0722*b)),200*(f(.2126*r+.7152*g+.0722*b)-f((.0193*r+.1192*g+.9505*b)/1.08883))];};
+ for(const matrix of [[[.367322,.860646,-.227968],[.280085,.672501,.047413],[-.011820,.042940,.968881]],[[.152286,1.052583,-.204868],[.114503,.786281,.099216],[-.003882,-.048116,1.051998]]]) {
+ const simulated=c=>lab(matrix.map(row=>Math.max(0,Math.min(1,row.reduce((n,v,i)=>n+v*linear(c[i]),0)))));
+ const a=simulated(success),b=simulated(failed);const delta=Math.hypot(...a.map((v,i)=>v-b[i]));assert.ok(delta>=20,`simulated ΔE ${delta}`);
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+test('filter state restored from a shared URL is not announced',async()=>{
+ const p=await page('?tab=mean-citedness&mc-kind=reproduction-numerical',390);
+ await p.waitForSelector('#mc-dist-chart-data');
+ assert.equal(await p.locator('#mc-study-type .chip.active').getAttribute('data-value'),'reproduction-numerical');
+ assert.equal((await p.locator('#analysis-feedback').textContent()).trim(),'');
+ await p.close();
+});
Authorship Overlap
Citation and reuse guidance
@@ -760,7 +768,7 @@Authorship Overlap and Outcomes
This may produce false positives for common family names when two unrelated researchers share a surname. -
+
@@ -794,11 +802,12 @@
+
+ Publication Type
Citation and reuse guidance
How to cite
Wallrich, L., & Röseler, L. (2026). FLoRA Explorer [Website]. https://forrt.org/flora-explorer/
-
+
@@ -996,13 +1005,13 @@ Outcomes by report size
-
+
+
diff --git a/tests/browser.test.cjs b/tests/browser.test.cjs
index f3091301..f242dca4 100644
--- a/tests/browser.test.cjs
+++ b/tests/browser.test.cjs
@@ -1020,3 +1020,183 @@ test('small-sample and unavailable-data messages use reader-facing words',async(
assert.doesNotMatch(await missing.locator('#kpis').innerText(),/workflow|secrets|GitHub|refresh-data/);
assert.deepEqual(missing.errors,[]);await missing.close();
});
+
+test('keyboard landmarks, citation sorting and paging retain focus and expose state',async()=>{
+ const p=await page('?tab=citations');
+ await p.waitForSelector('#pagination [aria-current="page"]');
+ assert.equal(await p.getByRole('navigation',{name:'Pagination',exact:true}).getAttribute('id'),'pagination');
+ assert.equal(await p.locator('header nav[aria-label="Explorer views"]').count(),1);
+ assert.equal(await p.locator('footer').count(),1);
+ await p.locator('.skip-link').focus();await p.keyboard.press('Enter');
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'main-content');
+ const year=p.locator('#originals-table .table-sort[data-sort="year"]');
+ await year.focus();await p.keyboard.press('Enter');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('#originals-table th[role="button"]').count(),0);
+ await p.keyboard.press('Space');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'ascending');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.sort),'year');
+ await p.getByRole('button',{name:'Sort by co-citation percentage',exact:true}).click();
+ assert.equal(await p.locator('.cocit-th').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('.cocit-sort-btn[data-sort="cocit_prop"]').getAttribute('aria-pressed'),'true');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),null);
+ await p.locator('#pagination [data-action="next"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'2');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.action),'next');
+ await p.locator('#pagination [data-action="previous"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'1');
+ assert.equal(await p.evaluate(()=>document.activeElement.getAttribute('aria-current')),'page');
+ await p.locator('#pagination button[data-p="3"]').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.p),'3');
+ assert.equal(await p.locator('#originals-table th').last().innerText(),'Timeline');
+ for(const size of await p.locator('#originals-table button[data-sort]').evaluateAll(els=>els.map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}))) assert.ok(size.every(v=>v>=24),`sort target ${size}`);
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('mobile paging focuses the results heading; counts and chip changes have one announcement',async()=>{
+ const p=await page('?tab=browse',390);
+ await p.locator('#bm-next').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'browse-results-heading');
+ assert.match(await p.locator('#browse-mobile-meta').innerText(),/Showing 21–40/);
+ assert.equal(await p.locator('#browse-mobile-meta').getAttribute('role'),'status');
+ const position=await p.locator('#browse-results-heading').boundingBox();
+ await p.waitForFunction(()=>document.getElementById('browse-results-heading').getBoundingClientRect().top<150);
+ assert.ok(position);
+ assert.equal(await p.locator('#browse-kind-count[role="status"]').count(),0);
+ await p.selectOption('#mobile-navigation','trends-tab');
+ await p.locator('#trends .trend-filter-btn[data-kind="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#trend-filter-count').getAttribute('role'),'status');
+ assert.match(await p.locator('#trend-filter-count').textContent(),/Numerical Reproductions/);
+ await p.selectOption('#mobile-navigation','mc-tab');
+ await p.waitForSelector('#mc-dist-chart-data');
+ await p.locator('#mc-study-type [data-value="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#analysis-feedback').textContent(),'Mean Citedness study type: Numerical Reproductions selected.');
+ await p.selectOption('#mobile-navigation','overview-tab');
+ const tip=p.locator('#overview .info-icon').first();await tip.focus();
+ assert.equal(await tip.getAttribute('role'),'img');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1');
+ await p.evaluate(()=>document.querySelector('.app-main').scrollTop+=100);
+ await p.waitForFunction(()=>document.querySelector('#overview .info-icon').classList.contains('tip-dismissed'));
+ assert.equal(await tip.locator('.info-tip').evaluate(el=>getComputedStyle(el).pointerEvents),'none');
+ assert.ok(await tip.evaluate(el=>el===document.activeElement));
+ // A touch activation must reopen the definition even when focus has not changed.
+ await tip.dispatchEvent('click');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1',null,{timeout:2000});
+ await p.keyboard.press('Escape');
+ assert.ok(await tip.evaluate(el=>el.classList.contains('tip-dismissed')));
+ await tip.dispatchEvent('focus');
+ assert.ok(await tip.evaluate(el=>!el.classList.contains('tip-dismissed')));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse empty results announce the explanation on desktop and mobile',async()=>{
+ const p=await page('?tab=browse');
+ for(const width of [1280,390]) {
+ await p.setViewportSize({width,height:844});
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ const empty=p.locator('#browse-empty');
+ assert.ok(await empty.isVisible());
+ assert.equal(await empty.getAttribute('role'),'status');
+ assert.match(await empty.innerText(),/No reference pairs match/);
+ await empty.getByRole('button',{name:'Clear search',exact:true}).click();
+ assert.ok(await empty.isHidden());
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse URL kinds and zero-result announcements match the filtered studies',async()=>{
+ for(const [kind,label,expectedKind] of [
+ ['all','All studies','all'],
+ ['replication','Replications','replication'],
+ ['reproduction','Reproductions','reproduction'],
+ ['reproduction-numerical','Numerical Reproductions','reproduction-numerical'],
+ ['reproduction-robustness','Robustness Reproductions','reproduction-robustness'],
+ ['unknown','All studies','all'],
+ ]) {
+ const p=await page('?tab=browse&kind='+kind,390);
+ assert.equal(await p.evaluate(()=>browseKind),expectedKind,kind);
+ assert.equal(await p.locator('#browse-mobile-meta .visually-hidden').textContent(),label+': ',kind);
+ const counts=await p.evaluate(kind=>[getChartData().length,filterByKind(fullRowData,kind).length],expectedKind);
+ assert.ok(counts[0]>0,kind);assert.equal(counts[0],counts[1],kind);
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ assert.equal(await p.locator('#browse-mobile-meta').textContent(),label+': 0 reference pairs',kind);
+ assert.deepEqual(p.errors,[]);await p.close();
+ }
+});
+
+test('chart alternatives and actions have distinct names; motion and forced colours respect preferences',async()=>{
+ const p=await page('?tab=overview',1280,p=>p.emulateMedia({reducedMotion:'reduce'}));
+ assert.equal(await p.evaluate(()=>Chart.getChart('overview-outcome-chart').options.animation),false);
+ assert.match(await p.evaluate(()=>Chart.defaults.font.family),/Source Sans 3/);
+ assert.notEqual(await p.locator('#citation-copy-btn').getAttribute('aria-label'),await p.locator('#website-citation-copy-btn').getAttribute('aria-label'));
+ for(const tab of ['browse','trends','citation','mc','overlap','pub']) {
+ await p.click('#'+tab+'-tab');await p.waitForTimeout(600);
+ assert.equal(await p.locator('.tab-pane.active h1').count(),1,tab);
+ }
+ await p.waitForSelector('#ls-chart-data');
+ for(const selector of ['.chart-copy','.chart-data summary','.chart-data [role="region"]','.chart-mode']) {
+ const names=await p.locator(selector).evaluateAll(els=>els.map(e=>e.getAttribute('aria-label')));
+ assert.ok(names.length>0,`${selector}: no controls found`);
+ assert.ok(names.every(Boolean),selector);assert.equal(new Set(names).size,names.length,`${selector}: ${names}`);
+ }
+ await p.emulateMedia({forcedColors:'active'});
+ const selected=p.locator('#pubtype-study-type .chip.active');
+ assert.equal(await selected.evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ assert.equal(await p.locator('#pub-tab').evaluate(el=>getComputedStyle(el).borderBottomStyle),'double');
+ await p.click('#citation-tab');
+ await p.waitForSelector('.rep-seg');
+ const fills={};
+ for(const outcome of ['successful','failed','mixed']) {
+ // Filtering to an outcome guarantees its segment is present on this page.
+ await p.selectOption('#filter-outcome',outcome);
+ const segment=p.locator('.rep-seg.'+outcome).first();
+ assert.ok(await segment.isVisible(),outcome);
+ fills[outcome]=await segment.evaluate(el=>getComputedStyle(el).backgroundColor+' '+getComputedStyle(el).backgroundImage);
+ }
+ assert.equal(new Set(Object.values(fills)).size,3);
+ assert.match(fills.successful,/repeating-linear-gradient/);
+ assert.ok(await p.locator('.rep-seg').first().evaluate(el=>getComputedStyle(el).forcedColorAdjust==='none'));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('focus rings, small targets and the dark success/failure pair keep sufficient contrast',async()=>{
+ const p=await page('?tab=overview');
+ const linear=v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4;};
+ const rgb=hex=>hex.match(/[a-f\d]{2}/gi).map(x=>parseInt(x,16));
+ const lum=c=>c.map(linear).reduce((n,v,i)=>n+v*[.2126,.7152,.0722][i],0);
+ const contrast=(a,b)=>(Math.max(lum(a),lum(b))+.05)/(Math.min(lum(a),lum(b))+.05);
+ for(const theme of ['light','dark']) {
+ if(await p.evaluate(()=>currentTheme())!==theme) await p.click('#theme-toggle');
+ for(const selector of ['#theme-toggle','.brand-mark','#overview-tab']) {
+ await p.keyboard.press('Tab');await p.locator(selector).focus();
+ assert.equal(await p.locator(selector).evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ const colours=await p.locator(selector).evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(document.querySelector('.top-bar')).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...colours)>=3,`${theme} ${selector}`);
+ }
+ await p.click('#citation-tab');await p.waitForSelector('#pagination button');
+ await p.locator('#search-input').focus();
+ const inputColours=await p.locator('#search-input').evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(el).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...inputColours)>=3);
+ const small=await p.locator('.info-icon, .gloss, footer a, .modal-close, #originals-table button[data-sort]').evaluateAll(els=>els.filter(el=>el.getClientRects().length).map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}));
+ assert.ok(small.every(s=>s.every(v=>v>=24)),JSON.stringify(small));
+ await p.click('#overview-tab');
+ }
+ const colours=await p.evaluate(()=>['--color-chart-successful','--color-chart-failed','--color-surface-raised'].map(t=>FloraCharts.token(t)));
+ const [success,failed,bg]=colours.map(rgb);assert.ok(contrast(success,bg)>=3&&contrast(failed,bg)>=3);
+ // Same full-severity Machado matrices and CIE76 Lab distance used by the review.
+ const lab=([r,g,b])=>{const f=t=>t>.008856?Math.cbrt(t):7.787*t+16/116;return [116*f(.2126*r+.7152*g+.0722*b)-16,500*(f((.4124*r+.3576*g+.1805*b)/.95047)-f(.2126*r+.7152*g+.0722*b)),200*(f(.2126*r+.7152*g+.0722*b)-f((.0193*r+.1192*g+.9505*b)/1.08883))];};
+ for(const matrix of [[[.367322,.860646,-.227968],[.280085,.672501,.047413],[-.011820,.042940,.968881]],[[.152286,1.052583,-.204868],[.114503,.786281,.099216],[-.003882,-.048116,1.051998]]]) {
+ const simulated=c=>lab(matrix.map(row=>Math.max(0,Math.min(1,row.reduce((n,v,i)=>n+v*linear(c[i]),0)))));
+ const a=simulated(success),b=simulated(failed);const delta=Math.hypot(...a.map((v,i)=>v-b[i]));assert.ok(delta>=20,`simulated ΔE ${delta}`);
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+test('filter state restored from a shared URL is not announced',async()=>{
+ const p=await page('?tab=mean-citedness&mc-kind=reproduction-numerical',390);
+ await p.waitForSelector('#mc-dist-chart-data');
+ assert.equal(await p.locator('#mc-study-type .chip.active').getAttribute('data-value'),'reproduction-numerical');
+ assert.equal((await p.locator('#analysis-feedback').textContent()).trim(),'');
+ await p.close();
+});
Citation and reuse guidance
How to cite
Wallrich, L., & Röseler, L. (2026). FLoRA Explorer [Website]. https://forrt.org/flora-explorer/
@@ -996,13 +1005,13 @@
- Outcomes by report size
+
+
diff --git a/tests/browser.test.cjs b/tests/browser.test.cjs
index f3091301..f242dca4 100644
--- a/tests/browser.test.cjs
+++ b/tests/browser.test.cjs
@@ -1020,3 +1020,183 @@ test('small-sample and unavailable-data messages use reader-facing words',async(
assert.doesNotMatch(await missing.locator('#kpis').innerText(),/workflow|secrets|GitHub|refresh-data/);
assert.deepEqual(missing.errors,[]);await missing.close();
});
+
+test('keyboard landmarks, citation sorting and paging retain focus and expose state',async()=>{
+ const p=await page('?tab=citations');
+ await p.waitForSelector('#pagination [aria-current="page"]');
+ assert.equal(await p.getByRole('navigation',{name:'Pagination',exact:true}).getAttribute('id'),'pagination');
+ assert.equal(await p.locator('header nav[aria-label="Explorer views"]').count(),1);
+ assert.equal(await p.locator('footer').count(),1);
+ await p.locator('.skip-link').focus();await p.keyboard.press('Enter');
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'main-content');
+ const year=p.locator('#originals-table .table-sort[data-sort="year"]');
+ await year.focus();await p.keyboard.press('Enter');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('#originals-table th[role="button"]').count(),0);
+ await p.keyboard.press('Space');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),'ascending');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.sort),'year');
+ await p.getByRole('button',{name:'Sort by co-citation percentage',exact:true}).click();
+ assert.equal(await p.locator('.cocit-th').getAttribute('aria-sort'),'descending');
+ assert.equal(await p.locator('.cocit-sort-btn[data-sort="cocit_prop"]').getAttribute('aria-pressed'),'true');
+ assert.equal(await year.locator('..').getAttribute('aria-sort'),null);
+ await p.locator('#pagination [data-action="next"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'2');
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.action),'next');
+ await p.locator('#pagination [data-action="previous"]').click();
+ assert.equal(await p.locator('#pagination [aria-current]').textContent(),'1');
+ assert.equal(await p.evaluate(()=>document.activeElement.getAttribute('aria-current')),'page');
+ await p.locator('#pagination button[data-p="3"]').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.dataset.p),'3');
+ assert.equal(await p.locator('#originals-table th').last().innerText(),'Timeline');
+ for(const size of await p.locator('#originals-table button[data-sort]').evaluateAll(els=>els.map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}))) assert.ok(size.every(v=>v>=24),`sort target ${size}`);
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('mobile paging focuses the results heading; counts and chip changes have one announcement',async()=>{
+ const p=await page('?tab=browse',390);
+ await p.locator('#bm-next').click();
+ assert.equal(await p.evaluate(()=>document.activeElement.id),'browse-results-heading');
+ assert.match(await p.locator('#browse-mobile-meta').innerText(),/Showing 21–40/);
+ assert.equal(await p.locator('#browse-mobile-meta').getAttribute('role'),'status');
+ const position=await p.locator('#browse-results-heading').boundingBox();
+ await p.waitForFunction(()=>document.getElementById('browse-results-heading').getBoundingClientRect().top<150);
+ assert.ok(position);
+ assert.equal(await p.locator('#browse-kind-count[role="status"]').count(),0);
+ await p.selectOption('#mobile-navigation','trends-tab');
+ await p.locator('#trends .trend-filter-btn[data-kind="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#trend-filter-count').getAttribute('role'),'status');
+ assert.match(await p.locator('#trend-filter-count').textContent(),/Numerical Reproductions/);
+ await p.selectOption('#mobile-navigation','mc-tab');
+ await p.waitForSelector('#mc-dist-chart-data');
+ await p.locator('#mc-study-type [data-value="reproduction-numerical"]').click();
+ assert.equal(await p.locator('#analysis-feedback').textContent(),'Mean Citedness study type: Numerical Reproductions selected.');
+ await p.selectOption('#mobile-navigation','overview-tab');
+ const tip=p.locator('#overview .info-icon').first();await tip.focus();
+ assert.equal(await tip.getAttribute('role'),'img');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1');
+ await p.evaluate(()=>document.querySelector('.app-main').scrollTop+=100);
+ await p.waitForFunction(()=>document.querySelector('#overview .info-icon').classList.contains('tip-dismissed'));
+ assert.equal(await tip.locator('.info-tip').evaluate(el=>getComputedStyle(el).pointerEvents),'none');
+ assert.ok(await tip.evaluate(el=>el===document.activeElement));
+ // A touch activation must reopen the definition even when focus has not changed.
+ await tip.dispatchEvent('click');
+ await p.waitForFunction(()=>getComputedStyle(document.querySelector('#overview .info-tip')).opacity==='1',null,{timeout:2000});
+ await p.keyboard.press('Escape');
+ assert.ok(await tip.evaluate(el=>el.classList.contains('tip-dismissed')));
+ await tip.dispatchEvent('focus');
+ assert.ok(await tip.evaluate(el=>!el.classList.contains('tip-dismissed')));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse empty results announce the explanation on desktop and mobile',async()=>{
+ const p=await page('?tab=browse');
+ for(const width of [1280,390]) {
+ await p.setViewportSize({width,height:844});
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ const empty=p.locator('#browse-empty');
+ assert.ok(await empty.isVisible());
+ assert.equal(await empty.getAttribute('role'),'status');
+ assert.match(await empty.innerText(),/No reference pairs match/);
+ await empty.getByRole('button',{name:'Clear search',exact:true}).click();
+ assert.ok(await empty.isHidden());
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('Browse URL kinds and zero-result announcements match the filtered studies',async()=>{
+ for(const [kind,label,expectedKind] of [
+ ['all','All studies','all'],
+ ['replication','Replications','replication'],
+ ['reproduction','Reproductions','reproduction'],
+ ['reproduction-numerical','Numerical Reproductions','reproduction-numerical'],
+ ['reproduction-robustness','Robustness Reproductions','reproduction-robustness'],
+ ['unknown','All studies','all'],
+ ]) {
+ const p=await page('?tab=browse&kind='+kind,390);
+ assert.equal(await p.evaluate(()=>browseKind),expectedKind,kind);
+ assert.equal(await p.locator('#browse-mobile-meta .visually-hidden').textContent(),label+': ',kind);
+ const counts=await p.evaluate(kind=>[getChartData().length,filterByKind(fullRowData,kind).length],expectedKind);
+ assert.ok(counts[0]>0,kind);assert.equal(counts[0],counts[1],kind);
+ await p.fill('#browse-mobile-input','no-matching-flora-reference-zzzz');
+ await p.waitForFunction(()=>getChartData().length===0);
+ assert.equal(await p.locator('#browse-mobile-meta').textContent(),label+': 0 reference pairs',kind);
+ assert.deepEqual(p.errors,[]);await p.close();
+ }
+});
+
+test('chart alternatives and actions have distinct names; motion and forced colours respect preferences',async()=>{
+ const p=await page('?tab=overview',1280,p=>p.emulateMedia({reducedMotion:'reduce'}));
+ assert.equal(await p.evaluate(()=>Chart.getChart('overview-outcome-chart').options.animation),false);
+ assert.match(await p.evaluate(()=>Chart.defaults.font.family),/Source Sans 3/);
+ assert.notEqual(await p.locator('#citation-copy-btn').getAttribute('aria-label'),await p.locator('#website-citation-copy-btn').getAttribute('aria-label'));
+ for(const tab of ['browse','trends','citation','mc','overlap','pub']) {
+ await p.click('#'+tab+'-tab');await p.waitForTimeout(600);
+ assert.equal(await p.locator('.tab-pane.active h1').count(),1,tab);
+ }
+ await p.waitForSelector('#ls-chart-data');
+ for(const selector of ['.chart-copy','.chart-data summary','.chart-data [role="region"]','.chart-mode']) {
+ const names=await p.locator(selector).evaluateAll(els=>els.map(e=>e.getAttribute('aria-label')));
+ assert.ok(names.length>0,`${selector}: no controls found`);
+ assert.ok(names.every(Boolean),selector);assert.equal(new Set(names).size,names.length,`${selector}: ${names}`);
+ }
+ await p.emulateMedia({forcedColors:'active'});
+ const selected=p.locator('#pubtype-study-type .chip.active');
+ assert.equal(await selected.evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ assert.equal(await p.locator('#pub-tab').evaluate(el=>getComputedStyle(el).borderBottomStyle),'double');
+ await p.click('#citation-tab');
+ await p.waitForSelector('.rep-seg');
+ const fills={};
+ for(const outcome of ['successful','failed','mixed']) {
+ // Filtering to an outcome guarantees its segment is present on this page.
+ await p.selectOption('#filter-outcome',outcome);
+ const segment=p.locator('.rep-seg.'+outcome).first();
+ assert.ok(await segment.isVisible(),outcome);
+ fills[outcome]=await segment.evaluate(el=>getComputedStyle(el).backgroundColor+' '+getComputedStyle(el).backgroundImage);
+ }
+ assert.equal(new Set(Object.values(fills)).size,3);
+ assert.match(fills.successful,/repeating-linear-gradient/);
+ assert.ok(await p.locator('.rep-seg').first().evaluate(el=>getComputedStyle(el).forcedColorAdjust==='none'));
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+
+test('focus rings, small targets and the dark success/failure pair keep sufficient contrast',async()=>{
+ const p=await page('?tab=overview');
+ const linear=v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4;};
+ const rgb=hex=>hex.match(/[a-f\d]{2}/gi).map(x=>parseInt(x,16));
+ const lum=c=>c.map(linear).reduce((n,v,i)=>n+v*[.2126,.7152,.0722][i],0);
+ const contrast=(a,b)=>(Math.max(lum(a),lum(b))+.05)/(Math.min(lum(a),lum(b))+.05);
+ for(const theme of ['light','dark']) {
+ if(await p.evaluate(()=>currentTheme())!==theme) await p.click('#theme-toggle');
+ for(const selector of ['#theme-toggle','.brand-mark','#overview-tab']) {
+ await p.keyboard.press('Tab');await p.locator(selector).focus();
+ assert.equal(await p.locator(selector).evaluate(el=>getComputedStyle(el).outlineStyle),'solid');
+ const colours=await p.locator(selector).evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(document.querySelector('.top-bar')).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...colours)>=3,`${theme} ${selector}`);
+ }
+ await p.click('#citation-tab');await p.waitForSelector('#pagination button');
+ await p.locator('#search-input').focus();
+ const inputColours=await p.locator('#search-input').evaluate(el=>[getComputedStyle(el).outlineColor,getComputedStyle(el).backgroundColor].map(c=>c.match(/[\d.]+/g).slice(0,3).map(Number)));
+ assert.ok(contrast(...inputColours)>=3);
+ const small=await p.locator('.info-icon, .gloss, footer a, .modal-close, #originals-table button[data-sort]').evaluateAll(els=>els.filter(el=>el.getClientRects().length).map(el=>{const r=el.getBoundingClientRect();return [r.width,r.height];}));
+ assert.ok(small.every(s=>s.every(v=>v>=24)),JSON.stringify(small));
+ await p.click('#overview-tab');
+ }
+ const colours=await p.evaluate(()=>['--color-chart-successful','--color-chart-failed','--color-surface-raised'].map(t=>FloraCharts.token(t)));
+ const [success,failed,bg]=colours.map(rgb);assert.ok(contrast(success,bg)>=3&&contrast(failed,bg)>=3);
+ // Same full-severity Machado matrices and CIE76 Lab distance used by the review.
+ const lab=([r,g,b])=>{const f=t=>t>.008856?Math.cbrt(t):7.787*t+16/116;return [116*f(.2126*r+.7152*g+.0722*b)-16,500*(f((.4124*r+.3576*g+.1805*b)/.95047)-f(.2126*r+.7152*g+.0722*b)),200*(f(.2126*r+.7152*g+.0722*b)-f((.0193*r+.1192*g+.9505*b)/1.08883))];};
+ for(const matrix of [[[.367322,.860646,-.227968],[.280085,.672501,.047413],[-.011820,.042940,.968881]],[[.152286,1.052583,-.204868],[.114503,.786281,.099216],[-.003882,-.048116,1.051998]]]) {
+ const simulated=c=>lab(matrix.map(row=>Math.max(0,Math.min(1,row.reduce((n,v,i)=>n+v*linear(c[i]),0)))));
+ const a=simulated(success),b=simulated(failed);const delta=Math.hypot(...a.map((v,i)=>v-b[i]));assert.ok(delta>=20,`simulated ΔE ${delta}`);
+ }
+ assert.deepEqual(p.errors,[]);await p.close();
+});
+test('filter state restored from a shared URL is not announced',async()=>{
+ const p=await page('?tab=mean-citedness&mc-kind=reproduction-numerical',390);
+ await p.waitForSelector('#mc-dist-chart-data');
+ assert.equal(await p.locator('#mc-study-type .chip.active').getAttribute('data-value'),'reproduction-numerical');
+ assert.equal((await p.locator('#analysis-feedback').textContent()).trim(),'');
+ await p.close();
+});