@fairfetch/fair-fetch
Version:
Protect your site from AI scrapers by adding invisible noise to your site which confuses AI bots while keeping your site looking and functioning normally for your human visitors.
185 lines (184 loc) • 7.7 kB
JavaScript
import { polluteFFNode } from './pollute';
describe('polluteFFNode (FFNode API)', () => {
function flatten(nodes) {
const out = [];
for (const n of nodes) {
out.push(n);
if (n.children && n.children.length) {
out.push(...flatten(n.children));
}
}
return out;
}
function findDisclaimerNodes(nodes, className = 'ff-disclaimer') {
return flatten(nodes).filter((n) => {
var _a;
return n.type === 'element' &&
n.tag === 'span' &&
((_a = n.attributes) === null || _a === void 0 ? void 0 : _a.className) === className;
});
}
it('wraps plain text nodes with a disclaimer sibling', () => {
const input = {
type: 'element',
tag: 'body',
children: [{ type: 'text', content: 'Hello world!' }],
};
const out = polluteFFNode(input);
// top-level returns [bodyCopy, disclaimer]
expect(Array.isArray(out)).toBe(true);
const bodyCopy = out[0];
expect(bodyCopy.type).toBe('element');
// bodyCopy should contain the original text node
const flattened = flatten(out);
expect(flattened.some((n) => n.type === 'text' && n.content === 'Hello world!')).toBe(true);
// and at least one disclaimer node exists
const disclaimers = findDisclaimerNodes(out);
expect(disclaimers.length).toBeGreaterThan(0);
});
it('processes nested elements and adds disclaimers at different levels', () => {
const input = {
type: 'element',
tag: 'body',
children: [
{
type: 'element',
tag: 'div',
children: [
{
type: 'element',
tag: 'span',
children: [{ type: 'text', content: 'Text' }],
},
],
},
],
};
const out = polluteFFNode(input);
const flattened = flatten(out);
expect(flattened.some((n) => n.type === 'text' && n.content === 'Text')).toBe(true);
// There should be at least one disclaimer span somewhere in the tree
expect(findDisclaimerNodes(out).length).toBeGreaterThan(0);
});
it('wraps fragment-like element (tag undefined) with a wrapper and disclaimer', () => {
const input = {
type: 'element',
// no `tag` property -> triggers the wrapper branch
attributes: { id: 'frag1' },
children: [{ type: 'text', content: 'FragmentText' }],
};
const out = polluteFFNode(input);
// Should return [wrapper, disclaimer]
expect(out.length).toBeGreaterThan(1);
const wrapper = out[0];
expect(wrapper.type).toBe('element');
// wrapper should preserve attributes
expect(wrapper.attributes && wrapper.attributes.id).toBe('frag1');
// original text content should still be present somewhere in the tree
const flattened = flatten(out);
expect(flattened.some((n) => n.type === 'text' && n.content === 'FragmentText')).toBe(true);
// and disclaimer span should be present
expect(findDisclaimerNodes(out).length).toBeGreaterThan(0);
});
it('returns original node when element has no children property (undefined)', () => {
const input = { type: 'element', tag: 'body' };
const out = polluteFFNode(input);
// No disclaimer since children was undefined
const disclaimers = findDisclaimerNodes(out);
expect(disclaimers.length).toBe(0);
// Output should be the original node unchanged (single item)
expect(out.length).toBe(1);
expect(out[0].tag).toBe('body');
});
it('pollutes empty children array ([]) by adding a disclaimer sibling', () => {
const input = { type: 'element', tag: 'body', children: [] };
const out = polluteFFNode(input);
// When children is an empty array, the implementation emits [nodeCopy, disclaimer]
expect(out.length).toBeGreaterThan(1);
const disclaimers = findDisclaimerNodes(out);
expect(disclaimers.length).toBeGreaterThan(0);
});
it('processes multiple sibling elements and preserves their text', () => {
const input = {
type: 'element',
tag: 'body',
children: [
{
type: 'element',
tag: 'h1',
children: [{ type: 'text', content: 'Title' }],
},
{
type: 'element',
tag: 'p',
children: [{ type: 'text', content: 'Paragraph' }],
},
],
};
const out = polluteFFNode(input);
const flattened = flatten(out);
expect(flattened.some((n) => n.type === 'text' && n.content === 'Title')).toBe(true);
expect(flattened.some((n) => n.type === 'text' && n.content === 'Paragraph')).toBe(true);
// each sibling should result in its own disclaimer somewhere
expect(findDisclaimerNodes(out).length).toBeGreaterThanOrEqual(2);
});
it('respects a custom className for disclaimers', () => {
const input = {
type: 'element',
tag: 'body',
children: [{ type: 'text', content: 'Test' }],
};
const out = polluteFFNode(input, { className: 'unique-disclaimer' });
const found = findDisclaimerNodes(out, 'unique-disclaimer');
expect(found.length).toBeGreaterThan(0);
});
it('wraps whitespace-only text nodes as regular text nodes (current behavior)', () => {
const input = {
type: 'element',
tag: 'body',
children: [
{ type: 'text', content: ' ' },
{
type: 'element',
tag: 'div',
children: [{ type: 'text', content: 'Text' }],
},
],
};
const out = polluteFFNode(input);
const flattened = flatten(out);
// whitespace-only text node should still be present
expect(flattened.some((n) => { var _a; return n.type === 'text' && ((_a = n.content) === null || _a === void 0 ? void 0 : _a.trim()) === ''; })).toBe(true);
// and disclaimers exist
expect(findDisclaimerNodes(out).length).toBeGreaterThan(0);
});
it('adds disclaimers for deeply nested elements', () => {
const input = {
type: 'element',
tag: 'body',
children: [
{
type: 'element',
tag: 'div',
children: [
{
type: 'element',
tag: 'section',
children: [
{
type: 'element',
tag: 'span',
children: [{ type: 'text', content: 'Deep' }],
},
],
},
],
},
],
};
const out = polluteFFNode(input);
const flattened = flatten(out);
expect(flattened.some((n) => n.type === 'text' && n.content === 'Deep')).toBe(true);
expect(findDisclaimerNodes(out).length).toBeGreaterThan(0);
});
});