diff --git a/e2e/support/mock-supabase.ts b/e2e/support/mock-supabase.ts index f9ec8f0..c41451c 100644 --- a/e2e/support/mock-supabase.ts +++ b/e2e/support/mock-supabase.ts @@ -653,6 +653,16 @@ async function handleFeedbackRpc( sendJson(res, 200, [{ ...row }]); } +function handleDmojProblem(res: http.ServerResponse, problemCode: string): void { + if (provider === 'fail') { + sendJson(res, 503, { error: 'mock dmoj upstream failure' }); + return; + } + sendJson(res, 200, { + data: { object: { code: problemCode, name: 'Mock DMOJ Problem', types: ['Simulation'] } } + }); +} + function handleKattisPage(res: http.ServerResponse): void { if (provider === 'fail') { res.writeHead(500, { 'Content-Type': 'text/plain' }); @@ -786,6 +796,12 @@ const server = http.createServer(async (req, res) => { handleCodeforcesUserStatus(res); return; } + if (url.pathname.startsWith('/api/dmoj/problem/')) { + // DMOJ problem API (dmoj.ca/api/v2/problem/) redirected here. + await readBody(req); + handleDmojProblem(res, url.pathname.slice('/api/dmoj/problem/'.length)); + return; + } // Current-actor interaction reads and isolated writes. if (url.pathname === '/rest/v1/user_preferences') { diff --git a/playwright.config.ts b/playwright.config.ts index b4d1e01..7e19fc7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -67,10 +67,11 @@ const mockPreviewWebServer = { // deterministic Supabase storage key the auth-seeding init script targets. PUBLIC_SUPABASE_URL: MOCK_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY: 'mock-publishable-key', - // Redirect the server-side provider fetches (problemset / Kattis page) to - // the mock so no live provider is ever contacted during E2E. + // Redirect the server-side provider fetches (problemset / Kattis page / + // DMOJ API) to the mock so no live provider is ever contacted during E2E. PUBLIC_CODEFORCES_API_BASE: `${MOCK_SUPABASE_URL}/api`, - PUBLIC_KATTIS_BASE: MOCK_SUPABASE_URL + PUBLIC_KATTIS_BASE: MOCK_SUPABASE_URL, + PUBLIC_DMOJ_API_BASE: `${MOCK_SUPABASE_URL}/api/dmoj` } }; diff --git a/scripts/coverage-hooks.mjs b/scripts/coverage-hooks.mjs index 1072866..60ef790 100644 --- a/scripts/coverage-hooks.mjs +++ b/scripts/coverage-hooks.mjs @@ -9,7 +9,8 @@ const publicEnv = Object.freeze({ PUBLIC_SUPABASE_URL: process.env.PUBLIC_SUPABASE_URL ?? 'http://localhost', PUBLIC_SUPABASE_PUBLISHABLE_KEY: process.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? 'placeholder', PUBLIC_CODEFORCES_API_BASE: process.env.PUBLIC_CODEFORCES_API_BASE, - PUBLIC_KATTIS_BASE: process.env.PUBLIC_KATTIS_BASE + PUBLIC_KATTIS_BASE: process.env.PUBLIC_KATTIS_BASE, + PUBLIC_DMOJ_API_BASE: process.env.PUBLIC_DMOJ_API_BASE }); const virtualModules = new Map([ ['$app/environment', 'export const browser = false; export const dev = false;'], diff --git a/src/lib/collections/problemCollection.ts b/src/lib/collections/problemCollection.ts index 5763ec3..27bb07a 100644 --- a/src/lib/collections/problemCollection.ts +++ b/src/lib/collections/problemCollection.ts @@ -25,7 +25,7 @@ export type SolvedFilter = 'all' | 'solved' | 'unsolved'; export type ProblemSourceFilter = 'all' | Problem['source']; const SOLVED_FILTER_STATES = ['all', 'solved', 'unsolved'] as const; -const SOURCE_FILTER_STATES = ['all', 'codeforces', 'kattis'] as const; +const SOURCE_FILTER_STATES = ['all', 'codeforces', 'kattis', 'dmoj'] as const; type ProblemCollectionState = { sourceItems: readonly Problem[]; diff --git a/src/lib/components/ProblemTable.svelte b/src/lib/components/ProblemTable.svelte index 3955ced..dd6e69e 100644 --- a/src/lib/components/ProblemTable.svelte +++ b/src/lib/components/ProblemTable.svelte @@ -10,6 +10,12 @@ import TableFeedbackButtons from './TableFeedbackButtons.svelte'; const codeforcesLogo = '/images/codeforces.png'; const kattisLogo = '/images/kattis.png'; + const dmojLogo = '/images/dmoj.svg'; + const SOURCE_LOGOS: Record = { + codeforces: codeforcesLogo, + kattis: kattisLogo, + dmoj: dmojLogo + }; export let problems: Problem[] = []; export let bodyId: string | undefined = undefined; @@ -22,7 +28,7 @@ export let difficultySortDirection: SortDirection = null; export let solvedFilterState: 'all' | 'solved' | 'unsolved' = 'all'; export let authorFilterValue: string | null = null; - export let sourceFilterValue: 'all' | 'codeforces' | 'kattis' = 'all'; + export let sourceFilterValue: 'all' | Problem['source'] = 'all'; export let onLike: (problemId: string, isLike: boolean) => Promise; export let onToggleSolved: (problemId: string, isSolved: boolean) => Promise; export let onDifficultySort: () => void; @@ -74,6 +80,8 @@ function getDifficultyTooltip(problem: Problem): string { if (problem.source === 'kattis') { return `Kattis difficulty mapped from 1-10 scale to 800-3500 rating range`; + } else if (problem.source === 'dmoj') { + return `DMOJ scores problems in points, which are not comparable to the 800-3500 rating range`; } else { return `${getRatingTierName(problem.difficulty)}`; } @@ -180,6 +188,15 @@ > + {:else if sourceFilterValue === 'dmoj'} + {:else}