import { Page, Locator, expect } from '@playwright/test'; /** * Page Object Model for Challenge Detail page * URL: /chal/{id} */ export class ChallengePage { readonly page: Page; readonly verdictBadge: Locator; readonly runtimeText: Locator; readonly memoryText: Locator; readonly scoreText: Locator; readonly codeBlock: Locator; readonly subtaskResults: Locator; readonly testdataResults: Locator; readonly messageText: Locator; constructor(page: Page) { this.page = page; // Challenge result elements this.verdictBadge = page.locator('.badge, [class*="verdict"], [class*="state"]').first(); this.runtimeText = page.locator('text=/Runtime|執行時間/'); this.memoryText = page.locator('text=/Memory|記憶體/'); this.scoreText = page.locator('text=/Score|分數|Rate/'); this.codeBlock = page.locator('pre, code, .code-block'); this.subtaskResults = page.locator('[class*="subtask"]'); this.testdataResults = page.locator('[class*="testdata"]'); this.messageText = page.locator('.message, [class*="response"]'); } /** * Navigate to a specific challenge page */ async goto(challengeId: number) { await this.page.goto(`/chal/${challengeId}`); await this.page.waitForLoadState('networkidle'); } /** * Get the verdict/state of the challenge */ async getVerdict(): Promise { // Wait for verdict to be not "Challenging" or "Not Started" await this.page.waitForTimeout(1000); // Initial wait const verdictElement = this.verdictBadge.or( this.page.locator('text=/AC|WA|TLE|MLE|RE|CE|PE|IE/') ).first(); if (await verdictElement.count() === 0) { return 'UNKNOWN'; } const verdictText = await verdictElement.textContent(); return verdictText?.trim() || 'UNKNOWN'; } /** * Wait for challenge to finish judging * @param timeoutMs Maximum time to wait in milliseconds * @param pollIntervalMs How often to check the verdict */ async waitForJudgeComplete(timeoutMs: number = 60000, pollIntervalMs: number = 2000) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const verdict = await this.getVerdict(); // Check if judging is complete if (verdict !== 'Challenging' && verdict !== 'Not Started' && verdict !== 'UNKNOWN') { return verdict; } // Wait before next poll await this.page.waitForTimeout(pollIntervalMs); // Reload the page to get updated status await this.page.reload({ waitUntil: 'networkidle' }); } throw new Error(`Challenge did not complete within ${timeoutMs}ms`); } /** * Get runtime and memory usage */ async getPerformanceMetrics() { const metrics = { runtime: '', memory: '', }; if (await this.runtimeText.isVisible()) { const runtimeFull = await this.runtimeText.textContent(); metrics.runtime = runtimeFull?.match(/\d+/)?.[0] || ''; } if (await this.memoryText.isVisible()) { const memoryFull = await this.memoryText.textContent(); metrics.memory = memoryFull?.match(/\d+/)?.[0] || ''; } return metrics; } /** * Get the score/rate */ async getScore(): Promise { if (await this.scoreText.isVisible()) { const scoreText = await this.scoreText.textContent(); const scoreMatch = scoreText?.match(/\d+/); return scoreMatch?.[0] || '0'; } return '0'; } /** * Verify we are on the challenge page */ async verifyOnPage(challengeId: number) { await expect(this.page).toHaveURL(new RegExp(`/chal/${challengeId}`)); } /** * Check if subtask results are visible */ async hasSubtaskResults(): Promise { return await this.subtaskResults.count() > 0; } /** * Get challenge message (for errors, etc.) */ async getMessage(): Promise { if (await this.messageText.isVisible()) { return await this.messageText.textContent(); } return null; } }