diff --git a/.github/workflows/data-refresh-validation.yaml b/.github/workflows/data-refresh-validation.yaml index 026d9ee8..58e43bdf 100644 --- a/.github/workflows/data-refresh-validation.yaml +++ b/.github/workflows/data-refresh-validation.yaml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" cache: "npm" diff --git a/.github/workflows/data-refresh.yaml b/.github/workflows/data-refresh.yaml index 7492f92c..05d92aad 100644 --- a/.github/workflows/data-refresh.yaml +++ b/.github/workflows/data-refresh.yaml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" cache: "npm" @@ -69,10 +69,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" cache: "npm" diff --git a/src/urls.ts b/src/urls.ts index c584b809..d53bef55 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -122,7 +122,7 @@ export const getDownloadUrls = ({ }; export const getUserProfileUrl = ({ username }: { username: string }) => - new URL(`/users/${encodeURI(username)}/profile`, getArchiveBaseUrl()).href; + new URL(`/users/${encodeURIComponent(username)}/profile`, getArchiveBaseUrl()).href; const USER_WORKS_SORT_COLUMNS: Record = { authors: "authors_to_sort_on", @@ -145,11 +145,11 @@ export const getUserWorksUrl = ({ }: Partial & { username: string; pseud?: string }) => { // Pseud names are only unique within an account, so AO3 nests them under // each user. - const userPath = `/users/${encodeURI(username)}`; + const userPath = `/users/${encodeURIComponent(username)}`; const url = new URL( pseud === undefined ? `${userPath}/works` - : `${userPath}/pseuds/${encodeURI(pseud)}/works`, + : `${userPath}/pseuds/${encodeURIComponent(pseud)}/works`, getArchiveBaseUrl(), ); @@ -202,11 +202,11 @@ const REPLACE_TOKENS_REGEX = new RegExp( export const getTagUrl = (tagName: string) => new URL( - `tags/${encodeURI(tagName).replaceAll( + `tags/${encodeURIComponent(tagName.replaceAll( REPLACE_TOKENS_REGEX, (char: string) => isReplaceableToken(char) ? TOKEN_REPLACEMENTS_MAP[char] : char, - )}/`, + ))}/`, getArchiveBaseUrl(), ).href; diff --git a/tests/helpers/invariants.ts b/tests/helpers/invariants.ts index a4c1c003..66b377f9 100644 --- a/tests/helpers/invariants.ts +++ b/tests/helpers/invariants.ts @@ -1,10 +1,38 @@ import { expect } from "vitest"; +import { prettify } from "htmlfy"; + +export function expectHtml(html: string | null) { + return expect(html === null ? null : prettify(html)); +} interface DriftingCountMatchers { driftingCount(bounds: { atLeast: number; atMost: number }): R; } +interface HtmlMatchers { + toEqualHtml(expected: string | null): R; +} + expect.extend({ + toEqualHtml(received: unknown, expected: string | null) { + if ( + (received !== null && typeof received !== "string") || + (expected !== null && typeof expected !== "string") + ) { + throw new TypeError("toEqualHtml requires HTML strings or null"); + } + + const actualHtml = received === null ? null : prettify(received); + const expectedHtml = expected === null ? null : prettify(expected); + + return { + pass: actualHtml === expectedHtml, + message: () => + `expected HTML ${this.isNot ? "not " : ""}to match after formatting`, + actual: actualHtml, + expected: expectedHtml, + }; + }, driftingCount( received: unknown, bounds: Parameters[0], @@ -28,6 +56,8 @@ expect.extend({ }); declare module "vitest" { - interface Assertion extends DriftingCountMatchers {} - interface AsymmetricMatchersContaining extends DriftingCountMatchers {} + interface Assertion + extends DriftingCountMatchers, HtmlMatchers {} + interface AsymmetricMatchersContaining + extends DriftingCountMatchers, HtmlMatchers {} } diff --git a/tests/mocks/data/ao3/series/1728802/index.html b/tests/mocks/data/ao3/series/1728802/index.html index 1add3619..5d452e43 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 @@

Log In

@@ -445,13 +445,13 @@

Series
17
Kudos:
-
1,139
+
1,173
Bookmarks:
-
119
+
122
Hits:
-
11,909
+
12,211
@@ -492,13 +492,14 @@

Contact Us

  • Development

    -

    08 Jan 2023

    +

    09 Jan 2023

    @@ -694,13 +694,13 @@
    Series
    19
    Kudos:
    -
    485
    +
    497
    Bookmarks:
    -
    59
    +
    62
    Hits:
    -
    2,858
    +
    2,970
    @@ -741,13 +741,14 @@

    Contact Us

  • Development

    -

    09 Jun 2022

    +

    08 Jun 2022

    @@ -617,18 +617,20 @@
    Series
    1/1
    +
    Collections:
    +
    1
    Comments:
    31
    Kudos:
    -
    1,249
    +
    1,331
    Bookmarks:
    -
    211
    +
    220
    Hits:
    -
    10,257
    +
    10,844
    @@ -705,16 +707,16 @@
    Series
    2
    Comments:
    -
    51
    +
    54
    Kudos:
    -
    1,315
    +
    1,447
    Bookmarks:
    -
    186
    +
    204
    Hits:
    -
    9,087
    +
    9,934
    @@ -788,13 +790,13 @@
    Series
    68
    Kudos:
    -
    853
    +
    885
    Bookmarks:
    -
    140
    +
    142
    Hits:
    -
    7,052
    +
    7,927
    @@ -870,16 +872,16 @@
    Series
    1
    Comments:
    -
    64
    +
    66
    Kudos:
    -
    1,081
    +
    1,149
    Bookmarks:
    -
    140
    +
    149
    Hits:
    -
    7,035
    +
    7,620
    @@ -920,13 +922,14 @@

    Contact Us

  • Development

    @@ -2187,32 +2147,32 @@

  • @@ -2224,52 +2184,52 @@

  • @@ -2281,52 +2241,52 @@

  • @@ -2338,52 +2298,52 @@

  • -
  • -
  • +
  • +
  • @@ -2395,52 +2355,52 @@

  • @@ -2533,16 +2493,16 @@

    More Options

    + + @@ -2724,7 +2690,7 @@

    More Options

    -

    Navigation

    +

    Navigation

    @@ -2751,13 +2717,14 @@

    Contact Us

  • Development

    @@ -1936,52 +1929,52 @@

  • @@ -1993,52 +1986,52 @@

  • -
  • -
  • +
  • +
  • @@ -2050,52 +2043,52 @@

  • -
  • -
  • +
  • +
  • @@ -2123,27 +2116,27 @@

  • @@ -2155,32 +2148,32 @@

  • @@ -2192,32 +2185,32 @@

  • @@ -2229,52 +2222,52 @@

  • +
  • +
  • -
  • -
  • -
  • @@ -2286,52 +2279,52 @@

  • @@ -2343,52 +2336,52 @@

  • -
  • -
  • +
  • +
  • @@ -2400,52 +2393,52 @@

  • -
  • -
  • +
  • +
  • @@ -2538,16 +2531,16 @@

    More Options

    + + @@ -2729,7 +2728,7 @@

    More Options

    -

    Navigation

    +

    Navigation

    @@ -2756,13 +2755,14 @@

    Contact Us

  • Development

    -

    12 Jul 2026

    +

    06 Oct 2026

    Tags
    -
    Summary
    -
    -

    Al-haitham throws his beloved senior a surprise birthday dinner!

    -
    Language:
    -
    English
    +
    Türkçe
    Words:
    -
    1,221
    +
    3,434
    Chapters:
    -
    1/1
    +
    2/2
    @@ -1224,16 +1208,16 @@
    Summary
  • -
  • +
  • Tags
    Summary
    -

    娶了个行如芍药坐如牡丹花的贤惠老婆疑似莱茵哈鲁特这辈子在剑鬼老爷子眼里做对的唯一一件事。

    -

    *不定期更新的长篇连载
    *后续会有昴变回男生又变回女生……的情节
    *可能会随着剧情发展不断添加tag或修改分级

    +

    Un fanfic con varios oneshots de verity y thatmob, por ahora solo tiene contenido boy pussy y estare haciendo mas oneshots :)

    +

    ¡sugieran etiquetas!
    ¡abierto a sugerencias!

    @@ -1275,40 +1259,40 @@
    Summary
    Language:
    -
    中文-普通话 國語
    +
    Español
    Words:
    -
    13,261
    +
    19,584
    Chapters:
    -
    5/?
    +
    3/10
    Comments:
    -
    16
    +
    4
    Kudos:
    -
    329
    +
    49
    Bookmarks:
    -
    42
    +
    2
    Hits:
    -
    4,259
    +
    1,500
  • -
  • +
  • Tags
    Summary
    -

    Saiki Kusuo had already saved the world once.

    -

    After stopping a literal meteor, his psychic powers were back, PK Academy was as chaotic as ever, and all he wanted was a quiet afternoon with some coffee jelly.

    -

    When a misleading photograph threatens Teruhashi Kokomi's perfect reputation, one desperate lie leaves Saiki trapped in a fake relationship, surrounded by endless rumors, jealous classmates, and far more attention than any psychic should ever have to endure.

    -

    'Good grief.' He had already accepted that his life would never be ordinary. He just hadn't expected it to turn into a romantic comedy.

    +

    When sixteen-year-old Iwaizumi returns home from volleyball practice to find his fourteen-year-old brother Oikawa in a compromising position, years of repressed tension and teenage hormones erupt into a forbidden encounter that will change their relationship forever.

    +
    Series
    + @@ -1354,11 +1341,13 @@
    Summary
    Language:
    English
    Words:
    -
    3,230
    +
    1,424
    Chapters:
    1/1
    +
    Collections:
    +
    1
    @@ -1369,16 +1358,16 @@
    Summary
  • -
  • +
  • Tags
    Summary
    -

    A banquet, a drink, and one (un)fortunate mistake. Merlin awakens in Arthur's bed with little memory of the previous night, but with all of the physical reminders.

    While pretending it never happened works for Arthur, it's less easy for Merlin as he comes to realize that he is carrying more than just regrets.

    +

    So I know that it is mentioned that Cardan kinda knows about Jude’s Mithridatism. But it is not talked about enough and I stand by that fully.
    +So I wrote this to highlight that fact that Jude had trauma and Cardan is a sweetheart.
    +Jude overdoses on her nightly poison and all goes downhill from there(no one dies! Don’t worry!)

    @@ -1421,38 +1412,31 @@
    Summary
    Language:
    English
    Words:
    -
    40,498
    +
    1,264
    Chapters:
    -
    11/?
    - +
    1/1
    -
    Comments:
    -
    95
    -
    Kudos:
    -
    429
    -
    Bookmarks:
    -
    70
    Hits:
    -
    8,359
    +
    0
  • -
  • +
  • Tags
    Summary
    -

    "No.", Pete said. "Let me fucking go.“

    -

    Vegas turned around, his face sporting an angry look again. "Are you stupid? You crippled my father and turned him into a vegetable.“

    -

    Pete was not relieved in the slightest that Gun had survived.

    -

    "I did nothing that didn't benefit you, as far as I'm concerned.“

    -

    "It's not about me, pet. It's about you - how do you think you'll be able to walk around the streets after word gets out that you nearly killed the head of the minor family? Or you know, you might have as well killed him - he will not be waking up from his coma.“

    -

    With roses, chocolates and panties being thrown in front of his feet, Pete guessed. People wanting to kiss the hands that had put Lucifer himself into a deep slumber.

    Pete had escaped the island but he couldn't outrun his mother's fate.

    -

    Pre-written story, now revised and reuploaded.

    +

    Just the filth polluting my mind for Kinktober 2026

    -
    Series
    - @@ -1507,38 +1479,38 @@
    Series
    Language:
    English
    Words:
    -
    63,294
    +
    13,852
    Chapters:
    -
    19/?
    +
    4/26
    Comments:
    -
    74
    +
    2
    Kudos:
    -
    54
    +
    25
    Bookmarks:
    -
    7
    +
    1
    Hits:
    -
    1,803
    +
    848
  • -
  • +
  • Tags
    Summary
    -

    "I want your number.”

    -

    Megumi blinks. "…Sorry, what?”

    -

    “My prize,” Yūji says simply, like it’s obvious. “Your phone number.”

    -

    “You don’t even know if you’ll ever see me again,” Megumi points out, narrowing his eyes.

    -

    Yūji shrugs, completely unbothered. “Then I guess I’ll have to make sure I do.”

    -

    Just like that, Megumi feels the familiar heat creep up the back of his neck.

    -

    “So?” Yūji tilts his head, “A deal’s a deal, right?”

    -

    Megumi exhales slowly. This is ridiculous.

    -

    They met tonight. Got locked in a store because of Yūji’s incompetence. Nearly died via shopping carts.

    -

    And yet, “You’re persistent,” Megumi mutters.

    -

    Yūji beams. “I’ve been told it's a good character trait.”

    Megumi was only supposed to buy tea—he didn’t plan on getting stuck with Yūji, who smiles like sunshine and refuses to let him go.

    +

    Kinktober 2026.

    +

    31 filthy Villaneve one-shots for the 31 days of October… Enjoy, perverts.

    +

    (More tags will be added with every update)

    @@ -1591,38 +1555,38 @@
    Summary
    Language:
    English
    Words:
    -
    16,714
    +
    13,799
    Chapters:
    -
    5/6
    +
    6/31
    Comments:
    -
    16
    +
    65
    Kudos:
    -
    87
    +
    63
    Bookmarks:
    -
    13
    +
    7
    Hits:
    -
    918
    +
    1,082
  • -
  • +
  • Tags
    Summary
    -

    His skin was still feverish but Kinn's naked skin made him feel like he was set alight.

    -

    "Don't you like me, Kinn?", he asked quietly, pressing his lips against the hollow of Kinn's throat. He could feel Kinn's throat working beneath his lips as he swallowed.

    -

    "Not my type. I like them nice and sweet."

    -

    Porsche hummed and cupped Kinn, slowly tracing a thumb over the outline of his hard cock in his pants.

    -

    "This part seems to like them mean."

    Thirteen years ago, Porsche and Porchay Kittisawat died in a fire caused by a gas leak, alongside their parents and their unborn sister. A street-smart bartender without a past is forced to work as a bodyguard for Kinn Theerapanyakun, slowly losing his humanity and his heart along the way. A masked man, calling himself The Phoenix, starts to take out the people closest to the Theerapanyakuns and he will be coming for them next.

    -

    These three things are not related. (Or are they?)

    -

    Prewitten story, now reuploaded. A story with many twists and turns and a different take on the Kittisawat/Theerapanyakun family lore.

    +

    Born from the sacrifice of a fallen god and the infinite cosmic power of the Astral Sea, Celestria awakens in Lord Liu Kang's newly forged timeline as Earthrealm's ancient protector. She carries the weight of a thousand timelines—memories of wars she couldn't win, gods she couldn't save, and a love that was destroyed by corruption.

    But this era is different. This timeline is peaceful. And in the quiet spaces between duty and devotion, Celestria discovers what it truly means to belong.

    As the Lin Kuei fractures and Bi-Han's darkness rises, she must navigate the impossible: protecting a realm while her immortal heart finally learns to feel again. And when a young assassin with eyes like smoke promises to be her shield, she faces her greatest fear—that love, no matter how beautiful, cannot save her from the cycle of loss that has defined her eternity.

    Because sometimes, the most powerful protection isn't about preventing tragedy. It's about having someone stand beside you when it comes.

    Series
    @@ -1677,23 +1635,21 @@
    Series
    Language:
    English
    Words:
    -
    69,714
    +
    11,258
    Chapters:
    -
    18/?
    +
    5/6
    -
    Comments:
    -
    67
    Kudos:
    -
    68
    +
    2
    Bookmarks:
    -
    11
    +
    1
    Hits:
    -
    2,137
    +
    14
    @@ -1742,27 +1698,27 @@

  • @@ -1774,32 +1730,32 @@

  • @@ -1811,32 +1767,32 @@

  • @@ -1848,52 +1804,52 @@

  • @@ -1905,52 +1861,52 @@

  • @@ -1962,52 +1918,52 @@

  • @@ -2019,52 +1975,52 @@

  • @@ -2092,27 +2048,27 @@

  • @@ -2124,32 +2080,32 @@

  • @@ -2161,32 +2117,32 @@

  • @@ -2198,52 +2154,52 @@

  • @@ -2255,52 +2211,52 @@

  • @@ -2312,52 +2268,52 @@

  • @@ -2369,52 +2325,52 @@

  • @@ -2507,16 +2463,16 @@

    More Options

    + + @@ -2698,7 +2660,7 @@

    More Options

    -

    Navigation

    +

    Navigation

    @@ -2725,13 +2687,14 @@

    Contact Us

  • Development

    @@ -1925,52 +1930,52 @@

  • -
  • -
  • +
  • +
  • -
  • -
  • +
  • +
  • @@ -1982,52 +1987,52 @@

  • -
  • -
  • +
  • +
  • -
  • -
  • -
  • -
  • +
  • +
  • @@ -2039,52 +2044,52 @@

  • -
  • -
  • +
  • +
  • -
  • @@ -2112,27 +2117,27 @@

  • @@ -2144,32 +2149,32 @@

  • @@ -2181,32 +2186,32 @@

  • @@ -2218,52 +2223,52 @@

  • -
  • -
  • -
  • @@ -2275,52 +2280,52 @@

  • -
  • -
  • +
  • +
  • -
  • -
  • +
  • +
  • @@ -2332,52 +2337,52 @@

  • -
  • -
  • +
  • +
  • -
  • -
  • -
  • -
  • +
  • +
  • @@ -2389,52 +2394,52 @@

  • -
  • -
  • +
  • +
  • -
  • @@ -2527,16 +2532,16 @@

    More Options

    + + @@ -2718,7 +2729,7 @@

    More Options

    -

    Navigation

    +

    Navigation

    @@ -2745,13 +2756,14 @@

    Contact Us

  • Development

    @@ -1833,32 +1799,32 @@

  • @@ -1870,52 +1836,52 @@

  • -
  • -
  • +
  • +
  • @@ -1927,52 +1893,52 @@

  • @@ -1984,52 +1950,52 @@

  • @@ -2041,52 +2007,52 @@

  • @@ -2114,27 +2080,27 @@

  • @@ -2146,32 +2112,32 @@

  • @@ -2183,32 +2149,32 @@

  • @@ -2220,52 +2186,52 @@

  • -
  • -
  • +
  • +
  • @@ -2277,52 +2243,52 @@

  • @@ -2334,52 +2300,52 @@

  • @@ -2391,52 +2357,52 @@

  • @@ -2529,16 +2495,16 @@

    More Options

    + + @@ -2720,7 +2692,7 @@

    More Options

    -

    Navigation

    +

    Navigation

    @@ -2747,13 +2719,14 @@

    Contact Us

  • Development

    @@ -2061,7 +2075,7 @@