diff --git a/tests/failed-urls-reporter.ts b/tests/failed-urls-reporter.ts
new file mode 100644
index 00000000..6d4e97a4
--- /dev/null
+++ b/tests/failed-urls-reporter.ts
@@ -0,0 +1,30 @@
+import type { Reporter, TestModule } from "vitest/node";
+
+declare module "vitest" {
+ interface TaskMeta {
+ requestedUrls?: string[];
+ }
+}
+
+export default class FailedUrlsReporter implements Reporter {
+ onTestRunEnd(testModules: ReadonlyArray) {
+ const failed: { name: string; urls: string[] }[] = [];
+ for (const module of testModules) {
+ for (const test of module.children.allTests("failed")) {
+ const urls = test.meta().requestedUrls;
+ if (!urls || urls.length === 0) continue;
+ failed.push({ name: test.fullName, urls });
+ }
+ }
+
+ if (failed.length === 0) return;
+
+ console.log("\nURLs requested during failed tests:");
+ for (const { name, urls } of failed) {
+ console.log(` ${name}`);
+ for (const url of urls) {
+ console.log(` ${url}`);
+ }
+ }
+ }
+}
diff --git a/tests/helpers/invariants.ts b/tests/helpers/invariants.ts
new file mode 100644
index 00000000..a4c1c003
--- /dev/null
+++ b/tests/helpers/invariants.ts
@@ -0,0 +1,33 @@
+import { expect } from "vitest";
+
+interface DriftingCountMatchers {
+ driftingCount(bounds: { atLeast: number; atMost: number }): R;
+}
+
+expect.extend({
+ driftingCount(
+ received: unknown,
+ bounds: Parameters[0],
+ ) {
+ const { atLeast, atMost } = bounds;
+ const pass =
+ typeof received === "number" &&
+ Number.isFinite(received) &&
+ received >= atLeast &&
+ received <= atMost;
+ return {
+ pass,
+ message: () =>
+ pass
+ ? `expected ${received} not to be within [${atLeast}, ${atMost}]`
+ : `expected ${received} to be a number within [${atLeast}, ${atMost}]`,
+ actual: received,
+ expected: `number in [${atLeast}, ${atMost}]`,
+ };
+ },
+});
+
+declare module "vitest" {
+ interface Assertion extends DriftingCountMatchers {}
+ interface AsymmetricMatchersContaining extends DriftingCountMatchers {}
+}
diff --git a/tests/mocks/data/ao3/series/1728802/index.html b/tests/mocks/data/ao3/series/1728802/index.html
index d2920a6e..ae312980 100644
--- a/tests/mocks/data/ao3/series/1728802/index.html
+++ b/tests/mocks/data/ao3/series/1728802/index.html
@@ -34,7 +34,7 @@
-
+
@@ -57,7 +57,7 @@