Skip to content
Open
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
6 changes: 6 additions & 0 deletions apps/extension/src/tools/extract/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,19 @@ export function collectExtractDom(this: Element, options: CollectorOptions): Raw
}
chain.push(current);
const style = styleOf(current);
// Chromium ignores content-visibility on ordinary inline, boxless and
// non-cell table boxes. Keep their rendered contents extractable.
if (
current.hasAttribute("hidden") ||
current.hasAttribute("data-bsk-overlay") ||
current.hasAttribute("data-bsk-overlay-host") ||
current.tagName.toLowerCase() === "browser-skill-overlay" ||
closedDetailsContent(current) ||
style.display === "none" ||
(style.contentVisibility === "hidden" &&
!/^(inline( list-item)?|contents|(?:inline-)?table|table-(caption|row|row-group|header-group|footer-group|column|column-group)|ruby(-.*)?)$/.test(
style.display,
)) ||
(style.visibility === "collapse" &&
/^table-(row|row-group|header-group|footer-group|column|column-group)$/.test(
style.display,
Expand Down
33 changes: 33 additions & 0 deletions apps/extension/src/tools/extract/extract.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ const values = (outcome: Outcome) =>
outcome.result!.rows.map((row) => outcome.result!.columns.map((column) => row[column.key]));

const fixtures: Record<string, string> = {
contentVisibility: `<table id="t"><tr><td>Visible<div style="content-visibility: hidden"><span style="content-visibility: visible; visibility: visible">Hidden text</span></div></td><td><div style="content-visibility: auto; position: absolute; top: 100000px">Offscreen text</div></td></tr></table>
<div style="content-visibility: hidden"><table id="hiddenTable"><tr><td>Hidden table</td></tr></table><ul id="hiddenList"><li>Hidden item</li></ul></div>
<ul id="l"><li>Visible item<div style="content-visibility: hidden"><a href="/hidden">Hidden link</a></div></li></ul>`,
ignoredContentVisibility: `<table id="t" style="content-visibility: hidden"><caption style="content-visibility: hidden">Caption</caption><tbody style="content-visibility: hidden"><tr style="content-visibility: hidden"><td><span style="content-visibility: hidden">Inline text</span><div style="display: contents; content-visibility: hidden">Contents text</div></td><td><span style="display: inline list-item; content-visibility: hidden">Inline item</span></td><td style="content-visibility: hidden">Hidden cell</td></tr></tbody></table>`,
rows1000: plainTable(1000, 3),
wide: plainTable(600, 100),
template: cells(`<td>
Expand Down Expand Up @@ -269,6 +273,35 @@ describe.skipIf(!process.env.BSK_CLICK_CHROME)("real browser structured extracti
).toEqual(["Shipped", "42", "edited", "x"]);
});

it("omits content-visibility hidden text without dropping auto offscreen content", async () => {
expect(values(await extract("contentVisibility", { action: "table", selector: "#t" }))).toEqual(
[["Visible", "Offscreen text"]],
);
const list = await extract("contentVisibility", { action: "list", selector: "#l" });
expect(list.result!.rows).toEqual([{ text: "Visible item", url: null }]);
});

it("does not discover or extract containers inside content-visibility hidden subtrees", async () => {
const discovery = await extract("contentVisibility", { action: "discover" });
expect(discovery.result!.targets!.map((target) => target.name)).toEqual(["t", "l"]);
for (const [action, selector] of [
["table", "#hiddenTable"],
["list", "#hiddenList"],
] as const) {
expect((await extract("contentVisibility", { action, selector })).error).toMatch(
/^extract_target_hidden:/,
);
}
});

it("keeps text where content-visibility does not apply to the display type", async () => {
expect(
values(await extract("ignoredContentVisibility", { action: "table", selector: "#t" })),
).toEqual([["Inline textContents text", "Inline item", null]]);
const discovery = await extract("ignoredContentVisibility", { action: "discover" });
expect(discovery.result!.targets!.map((target) => target.name)).toEqual(["Caption"]);
});

it("charges the byte budget for emitted text, not template indentation", async () => {
const { result } = await extract("indented", {
action: "table",
Expand Down
7 changes: 7 additions & 0 deletions apps/extension/src/tools/extract/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ beforeEach(() => {
document.body.innerHTML = "";
});
describe("structured extraction facts", () => {
it.each(["hidden", "auto", "visible"])("respects content-visibility: %s", (visibility) => {
document.body.innerHTML = `<table id="t"><tr><td>Visible<div style="content-visibility: ${visibility}"><span style="content-visibility: visible; visibility: visible">Extra</span></div></td></tr></table>`;
expect(extract("#t").rows).toEqual([
{ c1: visibility === "hidden" ? "Visible" : "Visible\nExtra" },
]);
});

it("preserves identifiers, currency, multiline text, empty cells and row provenance", () => {
document.body.innerHTML = orders;
const result = extract("#orders");
Expand Down