diff --git a/site/assets/theme.css b/site/assets/theme.css index 8fbe7f6..c67586a 100644 --- a/site/assets/theme.css +++ b/site/assets/theme.css @@ -5,3 +5,4 @@ .ready-archive-page .competition-records{max-width:46rem;gap:1rem;margin-bottom:.75rem;display:grid}.ready-archive-page .competition-records .archive-record h3{color:var(--link);font:1.25rem/1.25 var(--font-name);margin:.2rem 0 0}.ready-archive-page .ready-reissue{border-top:1px dotted var(--line);margin-top:1rem;padding-top:.8rem}.ready-archive-page .ready-reissue p{margin:.3rem 0}.ready-archive-page .ready-reissue__status{min-height:1.2rem;color:var(--status);font:.72rem/1.5 var(--font-data)} .ready-archive-page .crew-nav a,.ready-archive-page .crew-nav button{align-items:center;display:inline-flex}.ready-archive-page .discord-login{gap:.45rem}.ready-archive-page .discord-login img{width:1.25rem;height:auto;flex:none} .ready-archive-page .roster{max-width:46rem;grid-template-columns:repeat(auto-fill,minmax(19rem,1fr));gap:1rem;margin:1rem 0 0;padding:0;list-style:none;display:grid}.ready-archive-page .roster-member{border:1px solid var(--line);background:var(--panel);grid-template-columns:auto minmax(0,1fr);align-items:start;gap:1rem;padding:1rem;display:grid}.ready-archive-page .roster-avatar{border:1px solid var(--line);width:3rem;height:3rem;object-fit:cover;background:#050505;flex:none;display:block}.ready-archive-page .roster-identity{min-width:0}.ready-archive-page .roster-member h3{color:var(--link);font:.95rem/1.25 var(--font-name);overflow-wrap:anywhere;margin:0}.ready-archive-page .roster-handle{color:var(--muted);font:.72rem/1.4 var(--font-data);overflow-wrap:anywhere;margin:.3rem 0 0}.ready-archive-page .roster-events{border-top:1px dotted var(--line);color:var(--status);font:.72rem/1.5 var(--font-data);gap:.2rem;margin:.65rem 0 0;padding:.6rem 0 0;list-style:none;display:grid}@media (width<=44rem){.ready-archive-page .roster{grid-template-columns:1fr}} +.ready-archive-page .placement-extra{color:var(--score);margin-top:.2rem;display:block} diff --git a/site/index.html b/site/index.html index fd994f5..8dfdaab 100644 --- a/site/index.html +++ b/site/index.html @@ -46,7 +46,7 @@

Competition results

Kaspersky{CTF} 2026

Division: North America, South America, Caribbean

-
Placement
3 / 361
+
Placement
3 / 3611 / 52 (Europe)
Solved
30 / 30
Score
4,413
@@ -82,7 +82,7 @@

Team

SHOOTTHEMESSENGER/Moo

spookymoo

- +
  • @@ -98,7 +98,7 @@

    Hoxed

    romil0xsec

    romil1998

    - +
  • @@ -114,7 +114,7 @@

    Deva_RP

    Antsy

    antsyy_

    - +
  • diff --git a/site/results/index.html b/site/results/index.html index d5fcc4e..b5ea0c1 100644 --- a/site/results/index.html +++ b/site/results/index.html @@ -34,7 +34,7 @@

    RESULT 2026 / team result

    Kaspersky{CTF} 2026

    Division: North America, South America, Caribbean

    -
    Placement
    3 / 361
    Solved
    30 / 30
    Score
    4,413
    +
    Placement
    3 / 3611 / 52 (Europe)
    Solved
    30 / 30
    Score
    4,413
    diff --git a/site/team/index.html b/site/team/index.html index 8efb735..2fb5a8f 100644 --- a/site/team/index.html +++ b/site/team/index.html @@ -38,7 +38,7 @@

    Roster

    SHOOTTHEMESSENGER/Moo

    spookymoo

    - +
  • @@ -54,7 +54,7 @@

    Hoxed

    romil0xsec

    romil1998

    -
    • Kaspersky{CTF} 2026
    +
    • Kaspersky{CTF} 2026
    • Cyber Apocalypse 2026 (1337_PwnSp4c3)
    • BushBash CTF 2026 (1337_PwnSp4c3)
  • @@ -70,7 +70,7 @@

    Deva_RP

    Antsy

    antsyy_

    -
    • Kaspersky{CTF} 2026
    +
    • Kaspersky{CTF} 2026
    • Cyber Apocalypse 2026 (1337_PwnSp4c3)
    • BushBash CTF 2026 (1337_PwnSp4c3)
  • diff --git a/tests/site-contract.test.mjs b/tests/site-contract.test.mjs index f2328da..3ec7229 100644 --- a/tests/site-contract.test.mjs +++ b/tests/site-contract.test.mjs @@ -82,13 +82,14 @@ test("the homepage removes the redundant path and publishes every verified resul assert.match(home, /5,997/); assert.match(home, /Kaspersky\{CTF\} 2026/); assert.match(home, /3 \/ 361/); + assert.match(home, /1 \/ 52 \(Europe\)<\/span>/); assert.match(home, /Division: North America, South America, Caribbean/); assert.match(home, /30 \/ 30/); assert.match(home, /4,413/); assert.equal((home.match(/RESULT 2026 \/ team result/g) ?? []).length, 1); assert.equal((home.match(/RESULT 2026 \/ member result with a prior team/g) ?? []).length, 2); assert.doesNotMatch(home, /KK \/ RESULT 2026/); - assert.equal((home.match(/1337_PwnSp4c3/g) ?? []).length, 2); + assert.equal((home.match(/Credited to 1337_PwnSp4c3<\/strong>/g) ?? []).length, 2); assert.ok( home.indexOf("Competition results") < home.indexOf("Recruitment"), "results must appear before Recruitment", @@ -109,6 +110,7 @@ test("the Results page contains the complete verified competition record", () => assert.match(results, /Open - International/); assert.match(results, /Kaspersky\{CTF\} 2026/); assert.match(results, /3 \/ 361/); + assert.match(results, /1 \/ 52 \(Europe\)<\/span>/); assert.match(results, /Division: North America, South America, Caribbean/); assert.match(results, /30 \/ 30/); assert.match(results, /4,413/); @@ -200,20 +202,32 @@ test("the application client has no modal controls", () => { }); test("the roster is published on the homepage and the team page", () => { + const kaspersky = "Kaspersky{CTF} 2026"; + const apocalypse = "Cyber Apocalypse 2026 (1337_PwnSp4c3)"; + const bushbash = "BushBash CTF 2026 (1337_PwnSp4c3)"; const members = [ - ["spookymoo", "SHOOTTHEMESSENGER/Moo"], - ["hoxed", "Hoxed"], - ["romil1998", "romil0xsec"], - ["deva_rp", "Deva_RP"], - ["antsyy_", "Antsy"], + ["spookymoo", "SHOOTTHEMESSENGER/Moo", [kaspersky, apocalypse, bushbash]], + ["hoxed", "Hoxed", [kaspersky]], + ["romil1998", "romil0xsec", [kaspersky, apocalypse, bushbash]], + ["deva_rp", "Deva_RP", [kaspersky]], + ["antsyy_", "Antsy", [kaspersky, apocalypse, bushbash]], ]; for (const page of ["index.html", "team/index.html"]) { const html = read(page); assert.equal((html.match(/class="roster-member"/g) ?? []).length, members.length, `${page} needs one card per player`); - for (const [file, name] of members) { + const cards = [...html.matchAll(/
  • [\s\S]*?<\/li>\s*<\/ul>/g)].map((m) => m[0]); + assert.equal(cards.length, members.length, `${page} needs one parsed card per player`); + for (const [index, [file, name, played]] of members.entries()) { assert.ok(html.includes(`src="/brand/team/${file}.png"`), `${page} is missing the ${file} avatar`); assert.ok(html.includes(`

    ${name}

    `), `${page} is missing the name ${name}`); assert.ok(html.includes(`

    ${file}

    `), `${page} is missing the handle ${file}`); + const card = cards[index]; + assert.ok(card.includes(file), `${page} card order drifted at ${file}`); + assert.deepEqual( + [...card.matchAll(/
  • ([^<]+)<\/li>/g)].map((m) => m[1]), + played, + `${page} lists the wrong competitions for ${file}`, + ); } for (const match of html.matchAll(/]*>/g)) { assert.match(match[0], /alt=""/, "avatars are decorative next to the name");