Skip to content

Commit 5b5b119

Browse files
namedgraphclaude
andcommitted
The empty-hidden view gets the spec its fix never had.
e66a69a stopped an ontology view declared ldh:showWhenEmpty false from being painted before its query could say whether it was empty, and shipped without a test. This is that test, left untracked in the working tree since. It measures at the DOM rather than by screenshot, and in two independent ways, because the defect is a single frame: a MutationObserver reads the display the block was inserted with - delivered at the microtask checkpoint after insertion and before any paint, so it IS what the browser would first have rendered - and a requestAnimationFrame sampler beside it asks whether any frame of the whole load ever gave the block a box. The Broader view on the same page is the control: a build that simply hid every declared view would satisfy the first assertion and fail that one. Unrelated to the block-controls chrome two commits back, and needed no adjustment for it: it names no element that change touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bf09132 commit 5b5b119

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

‎ui-tests/specs/view-empty.spec.mjs‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// What a view declaring ldh:showWhenEmpty false does to the page on its way to being empty.
2+
//
3+
// The block is injected into the content flow before its query has run, so the emptiness it is
4+
// hidden for is not yet known when it arrives. Deciding at insert time from the DECLARATION
5+
// rather than at results time from the count is what keeps the card out of the layout: the
6+
// results only ever bring it back.
7+
//
8+
// Measured at the DOM rather than by screenshot. A MutationObserver callback is delivered at the
9+
// microtask checkpoint that follows the insertion and precedes any paint, so the display it reads
10+
// IS the one the browser would first have rendered. The per-frame sampler beside it is the
11+
// independent witness: it asks whether any frame of the whole load ever gave the block a box.
12+
import { test, expect } from '../lib/console.mjs';
13+
import { goto } from '../lib/settle.mjs';
14+
import { concept, document as conceptDocument } from '../lib/taxonomy.mjs';
15+
import { BROADER, NARROWER, rowFor, rows, viewBlock } from '../lib/blocks.mjs';
16+
17+
const READ_MODE = 'https://w3id.org/atomgraph/client#ReadMode';
18+
const pageFor = name => `${conceptDocument(name)}?mode=${encodeURIComponent(READ_MODE)}`;
19+
20+
// Installed before any of the page's own script runs, so the first injected block is seen.
21+
const watchInjectedViews = page => page.addInitScript(() => {
22+
const SELECTOR = 'div.block[data-show-when-empty]';
23+
const properties = block => block.getAttribute('data-property');
24+
25+
window.__insertions = [];
26+
new MutationObserver(records => {
27+
for (const record of records) {
28+
for (const node of record.addedNodes) {
29+
if (!(node instanceof HTMLElement)) continue;
30+
const blocks = [...(node.matches(SELECTOR) ? [node] : []), ...node.querySelectorAll(SELECTOR)];
31+
for (const block of blocks) {
32+
window.__insertions.push({
33+
property: properties(block),
34+
showWhenEmpty: block.getAttribute('data-show-when-empty'),
35+
display: getComputedStyle(block).display,
36+
height: block.offsetHeight,
37+
});
38+
}
39+
}
40+
}
41+
// document, not documentElement: an init script runs before the document has one.
42+
}).observe(document, { childList: true, subtree: true });
43+
44+
window.__tallest = {};
45+
const sample = () => {
46+
for (const block of document.querySelectorAll(SELECTOR)) {
47+
const property = properties(block);
48+
window.__tallest[property] = Math.max(window.__tallest[property] ?? 0, block.offsetHeight);
49+
}
50+
requestAnimationFrame(sample);
51+
};
52+
requestAnimationFrame(sample);
53+
});
54+
55+
const insertionOf = (insertions, property) => insertions.find(insertion => insertion.property === property);
56+
57+
test.describe('a view declared not to show when empty', () => {
58+
// The declaration is the package's and the results are the query's, and neither depends on
59+
// who is reading - the same reasoning as concept-hierarchy, which covers these same blocks.
60+
test.beforeEach(async ({ page }, testInfo) => {
61+
test.skip(testInfo.project.name !== 'owner',
62+
'what the block declares and what its query returns are the same for either agent');
63+
await watchInjectedViews(page);
64+
});
65+
66+
test('never takes a box in the flow while it is empty', async ({ page }) => {
67+
// espresso is a leaf: nothing is narrower than it. Its Broader view on the same page does
68+
// have a result, so a build that simply hid every declared view would not pass this.
69+
await goto(page, pageFor('espresso'));
70+
71+
await expect(rowFor(viewBlock(page, BROADER), concept('coffee'))).toHaveCount(1);
72+
await expect(viewBlock(page, NARROWER)).toBeHidden();
73+
74+
const insertions = await page.evaluate(() => window.__insertions);
75+
const narrower = insertionOf(insertions, NARROWER);
76+
77+
// The block does arrive - this is about how it arrives, not about it being withheld.
78+
expect(narrower, `no ${NARROWER} block was injected at all`).toBeDefined();
79+
expect(narrower.showWhenEmpty).toBe('false');
80+
// The two ways of saying the same thing at the moment of insertion, before any paint.
81+
expect(narrower.display).toBe('none');
82+
expect(narrower.height).toBe(0);
83+
84+
// And no frame of the load ever gave it one, which is the flash itself.
85+
const tallest = await page.evaluate(() => window.__tallest);
86+
expect(tallest[NARROWER]).toBe(0);
87+
// The control: the view with a result did take a box.
88+
expect(tallest[BROADER]).toBeGreaterThan(0);
89+
});
90+
91+
test('is shown once its results say it has something to show', async ({ page }) => {
92+
// hot-drinks has two children, so the same block that stays hidden above has to come back
93+
// here - the hide is only correct while the reveal that answers it still fires.
94+
await goto(page, pageFor('hot-drinks'));
95+
96+
const narrower = viewBlock(page, NARROWER);
97+
await expect(narrower).toBeVisible();
98+
await expect(rows(narrower)).toHaveCount(2);
99+
100+
// It was hidden on arrival here too: the reveal is what made it visible, not a different
101+
// insertion path.
102+
const insertions = await page.evaluate(() => window.__insertions);
103+
expect(insertionOf(insertions, NARROWER).display).toBe('none');
104+
});
105+
});

0 commit comments

Comments
 (0)