149 lines
5.9 KiB
TypeScript
149 lines
5.9 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { analyzeTocPage } from '../src/services/analyzer.ts';
|
|
|
|
const SCOPE = '#list-chapter .chapter-archive-grid';
|
|
const PATTERN = 'https://novelbin\\.com/b/the-mech-touch/.*';
|
|
|
|
function makeHtml(opts: {
|
|
title?: string;
|
|
body?: string;
|
|
links?: string[];
|
|
extraHead?: string;
|
|
}) {
|
|
const links = (opts.links ?? [])
|
|
.map(href => `<a href="${href}">link</a>`)
|
|
.join('');
|
|
return `<html>
|
|
<head>
|
|
<title>${opts.title ?? 'Test Page'}</title>
|
|
${opts.extraHead ?? ''}
|
|
</head>
|
|
<body>${opts.body ?? ''}${links}</body>
|
|
</html>`;
|
|
}
|
|
|
|
describe('analyzer — Cloudflare detection', () => {
|
|
test('flags "Just a moment" title as Cloudflare', () => {
|
|
const html = makeHtml({ title: 'Just a moment...' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const cf = d.findings.find(f => f.check === 'Cloudflare challenge')!;
|
|
expect(cf.passed).toBe(false);
|
|
expect(d.likelyCause).toContain('Cloudflare');
|
|
});
|
|
|
|
test('flags #challenge-form element as Cloudflare', () => {
|
|
const html = makeHtml({ body: '<form id="challenge-form"></form>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const cf = d.findings.find(f => f.check === 'Cloudflare challenge')!;
|
|
expect(cf.passed).toBe(false);
|
|
});
|
|
|
|
test('does not flag a clean page as Cloudflare', () => {
|
|
const html = makeHtml({ title: 'The Mech Touch — Chapters', body: '<div id="list-chapter"><div class="chapter-archive-grid"></div></div>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const cf = d.findings.find(f => f.check === 'Cloudflare challenge')!;
|
|
expect(cf.passed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('analyzer — security block detection', () => {
|
|
test('flags "you have been blocked" body text', () => {
|
|
const html = makeHtml({ body: '<p>You have been blocked from accessing this site.</p>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'Security / bot block')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(d.likelyCause).toContain('Security block');
|
|
});
|
|
});
|
|
|
|
describe('analyzer — CAPTCHA detection', () => {
|
|
test('flags g-recaptcha div', () => {
|
|
const html = makeHtml({ body: '<div class="g-recaptcha" data-sitekey="abc"></div>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'CAPTCHA')!;
|
|
expect(check.passed).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('analyzer — login wall detection', () => {
|
|
test('flags password input', () => {
|
|
const html = makeHtml({ body: '<form><input type="password"/></form>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'Login wall')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(d.likelyCause).toContain('Login');
|
|
});
|
|
});
|
|
|
|
describe('analyzer — scope selector breakdown', () => {
|
|
test('reports root missing when neither segment exists', () => {
|
|
const html = makeHtml({ body: '<div id="something-else"></div>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'TOC scope selector')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(check.detail).toContain('#list-chapter');
|
|
expect(check.detail).toContain('not found anywhere');
|
|
expect(d.likelyCause).toContain('redesigned');
|
|
});
|
|
|
|
test('reports inner selector missing when root exists but child does not', () => {
|
|
const html = makeHtml({ body: '<div id="list-chapter"><ul><li>no grid here</li></ul></div>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'TOC scope selector')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(check.detail).toContain('#list-chapter');
|
|
expect(check.detail).toContain('.chapter-archive-grid');
|
|
expect(d.likelyCause).toContain('Inner selector');
|
|
});
|
|
|
|
test('notes when inner selector exists elsewhere on the page (wrong hierarchy)', () => {
|
|
const html = makeHtml({
|
|
body: '<div id="list-chapter"></div><div class="chapter-archive-grid"></div>',
|
|
});
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'TOC scope selector')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(check.detail).toContain('exists elsewhere');
|
|
});
|
|
|
|
test('passes when full scope matches', () => {
|
|
const html = makeHtml({
|
|
body: '<div id="list-chapter"><div class="chapter-archive-grid"></div></div>',
|
|
});
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'TOC scope selector')!;
|
|
expect(check.passed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('analyzer — link pattern check', () => {
|
|
test('finds matching links outside the scope when scope is missing', () => {
|
|
const links = [
|
|
'https://novelbin.com/b/the-mech-touch/chapter-1',
|
|
'https://novelbin.com/b/the-mech-touch/chapter-2',
|
|
];
|
|
const html = makeHtml({ body: '<div id="other">' + links.map(l => `<a href="${l}">ch</a>`).join('') + '</div>' });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'Link pattern (anywhere)')!;
|
|
expect(check.passed).toBe(true);
|
|
expect(check.detail).toContain('2 link');
|
|
expect(d.likelyCause).toContain('scope selector');
|
|
});
|
|
|
|
test('reports no matching links and samples what is there', () => {
|
|
const html = makeHtml({ links: ['https://other.com/a', 'https://other.com/b'] });
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN);
|
|
const check = d.findings.find(f => f.check === 'Link pattern (anywhere)')!;
|
|
expect(check.passed).toBe(false);
|
|
expect(check.detail).toContain('other.com');
|
|
});
|
|
});
|
|
|
|
describe('analyzer — actualUrl', () => {
|
|
test('includes the landed URL in the diagnosis', () => {
|
|
const html = makeHtml({});
|
|
const d = analyzeTocPage(html, SCOPE, PATTERN, 'https://novelbin.com/redirected');
|
|
expect(d.actualUrl).toBe('https://novelbin.com/redirected');
|
|
});
|
|
});
|