Address review findings on the lnw pointer read (#89)

- Share one lnw host regex between matches and the pointer validator so
  the two cannot drift apart.
- Resolve the pointer href against the page address before validating,
  so a relative pointer still yields its Series instead of silently
  resolving to type other.
- Pin pointer/breadcrumb agreement on the divergent page and pin that a
  pointer off-host or not /novel/<slug>/ resolves to other.
This commit is contained in:
2026-08-11 11:52:31 +07:00
parent a4da2e2f00
commit 8beaa4fd00
2 changed files with 64 additions and 16 deletions
+51 -7
View File
@@ -165,19 +165,63 @@ test("lightnovelworld.detect reads the Series identity from the page pointer on
test("lightnovelworld.detect falls back to the breadcrumb when the pointer is absent", () => {
reset();
elements = { "h1.entry-title": "My Longevity Simulation Chapter 1" };
const url = "https://lightnovelworld.net/my-longevity-simulation-chapter-1/";
// Same divergent page, both pointers from the real markup (research note
// §3): breadcrumb position 2 must say what the All Chapter anchor says.
attrEls = {
"a[aria-label='All Chapter']": {
getAttribute: () => "https://lightnovelworld.net/novel/immortality-simulator/",
},
};
const viaPointer = lightnovelworld.detect(loc(url));
attrEls = {
// Verbatim from the real page (research note §3): position 2 of the
// microdata BreadcrumbList is the Series.
'[itemtype="http://schema.org/BreadcrumbList"] a[itemprop="item"][href*="/novel/"]': {
getAttribute: () => "https://lightnovelworld.net/novel/immortality-simulator/",
},
};
const url = "https://lightnovelworld.net/my-longevity-simulation-chapter-1/";
const p = lightnovelworld.detect(loc(url));
const viaBreadcrumb = lightnovelworld.detect(loc(url));
assert.equal(viaBreadcrumb.type, "chapter");
assert.equal(viaBreadcrumb.seriesId, viaPointer.seriesId);
assert.equal(viaBreadcrumb.seriesUrl, viaPointer.seriesUrl);
assert.equal(viaBreadcrumb.seriesId, "immortality-simulator");
assert.equal(viaBreadcrumb.chapterSlug, "my-longevity-simulation");
});
test("lightnovelworld.detect resolves a relative pointer against the page address", () => {
// Defensive: every measured page ships an absolute pointer href, but a
// theme change could go relative — the pointer is still this page's own
// link back to its Series.
reset();
elements = { "h1.entry-title": "A Will Eternal Chapter 1298" };
attrEls = {
"a[aria-label='All Chapter']": { getAttribute: () => "/novel/a-will-eternal/" },
};
const p = lightnovelworld.detect(loc("https://lightnovelworld.net/a-will-eternal-chapter-1298/"));
assert.equal(p.type, "chapter");
assert.equal(p.seriesId, "immortality-simulator");
assert.equal(p.seriesUrl, "https://lightnovelworld.net/novel/immortality-simulator/");
assert.equal(p.chapterSlug, "my-longevity-simulation");
assert.equal(p.seriesId, "a-will-eternal");
assert.equal(p.seriesUrl, "https://lightnovelworld.net/novel/a-will-eternal/");
});
test("lightnovelworld.detect rejects a pointer that is not a /novel/ address on its own host", () => {
// Counterfactual pointers, exercising the criterion that a pointer "present
// but not parseable as /novel/<slug>/ on lightnovelworld.net" must not
// become a seriesUrl the backend is later asked to Poll: off-host, and
// on-host but the wrong path shape.
reset();
elements = { "h1.entry-title": "My Longevity Simulation Chapter 1" };
const url = "https://lightnovelworld.net/my-longevity-simulation-chapter-1/";
attrEls = {
"a[aria-label='All Chapter']": {
getAttribute: () => "https://evil.example/novel/immortality-simulator/",
},
};
assert.equal(lightnovelworld.detect(loc(url)).type, "other");
attrEls = {
"a[aria-label='All Chapter']": {
getAttribute: () => "https://lightnovelworld.net/fiction/immortality-simulator/",
},
};
assert.equal(lightnovelworld.detect(loc(url)).type, "other");
});
test("lightnovelworld.detect resolves to other when the page carries no pointer", () => {