Initialize

This commit is contained in:
ChenKaiLiuG
2026-01-29 11:48:45 +08:00
parent 8579a2f732
commit e139daa410
20 changed files with 1228 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Page Object Model for Challenge List page
* URL: /chal
*/
export class ChallengeListPage {
readonly page: Page;
readonly challengeRows: Locator;
readonly accountFilter: Locator;
readonly problemFilter: Locator;
readonly stateFilter: Locator;
readonly compilerFilter: Locator;
constructor(page: Page) {
this.page = page;
// Challenge list elements
this.challengeRows = page.locator('tr[data-challenge-id], .challenge-row, tbody tr');
// Filter elements
this.accountFilter = page.locator('input[name="account"], #account-filter');
this.problemFilter = page.locator('input[name="problem"], #problem-filter');
this.stateFilter = page.locator('select[name="state"], #state-filter');
this.compilerFilter = page.locator('select[name="compiler"], #compiler-filter');
}
/**
* Navigate to the challenge list page
*/
async goto() {
await this.page.goto('/chal');
await this.page.waitForLoadState('networkidle');
}
/**
* Get the latest challenge ID from the list
*/
async getLatestChallengeId(): Promise<number | null> {
// Look for challenge links in the format /chal/{id}
const firstChallengeLink = this.page.locator('a[href^="/chal/"]').first();
if (await firstChallengeLink.count() === 0) {
return null;
}
const href = await firstChallengeLink.getAttribute('href');
if (!href) return null;
const match = href.match(/\/chal\/(\d+)/);
return match ? parseInt(match[1]) : null;
}
/**
* Click on a challenge to view details
*/
async clickChallenge(challengeId: number) {
await this.page.click(`a[href="/chal/${challengeId}"]`);
await this.page.waitForLoadState('networkidle');
}
/**
* Filter challenges by account ID
*/
async filterByAccount(accountId: number) {
if (await this.accountFilter.isVisible()) {
await this.accountFilter.fill(accountId.toString());
await this.page.keyboard.press('Enter');
await this.page.waitForLoadState('networkidle');
}
}
/**
* Filter challenges by problem ID
*/
async filterByProblem(problemId: number) {
if (await this.problemFilter.isVisible()) {
await this.problemFilter.fill(problemId.toString());
await this.page.keyboard.press('Enter');
await this.page.waitForLoadState('networkidle');
}
}
/**
* Verify we are on the challenge list page
*/
async verifyOnPage() {
await expect(this.page).toHaveURL(/\/chal/);
}
}

142
pages/ChallengePage.ts Normal file
View File

@@ -0,0 +1,142 @@
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<string> {
// 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<string> {
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<boolean> {
return await this.subtaskResults.count() > 0;
}
/**
* Get challenge message (for errors, etc.)
*/
async getMessage(): Promise<string | null> {
if (await this.messageText.isVisible()) {
return await this.messageText.textContent();
}
return null;
}
}

81
pages/LoginPage.ts Normal file
View File

@@ -0,0 +1,81 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Page Object Model for Login/Sign page
* URL: /sign
*/
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly registerTab: Locator;
readonly registerNameInput: Locator;
readonly registerEmailInput: Locator;
readonly registerPasswordInput: Locator;
readonly registerButton: Locator;
constructor(page: Page) {
this.page = page;
// Login form elements
this.emailInput = page.locator('input[name="email"], input[type="email"]').first();
this.passwordInput = page.locator('input[name="password"], input[type="password"]').first();
this.loginButton = page.locator('button:has-text("登入"), button:has-text("Login")').first();
// Register form elements
this.registerTab = page.locator('text=註冊, text=Register').first();
this.registerNameInput = page.locator('input[name="name"]');
this.registerEmailInput = page.locator('input[name="email"]').nth(1);
this.registerPasswordInput = page.locator('input[name="password"]').nth(1);
this.registerButton = page.locator('button:has-text("註冊"), button:has-text("Register")');
}
/**
* Navigate to the login page
*/
async goto() {
await this.page.goto('/sign');
}
/**
* Login with email and password
*/
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
// Wait for navigation after login
await this.page.waitForLoadState('networkidle');
}
/**
* Register a new account
*/
async register(name: string, email: string, password: string) {
await this.registerTab.click();
await this.registerNameInput.fill(name);
await this.registerEmailInput.fill(email);
await this.registerPasswordInput.fill(password);
await this.registerButton.click();
// Wait for navigation after registration
await this.page.waitForLoadState('networkidle');
}
/**
* Check if login was successful by verifying user is redirected
*/
async verifyLoginSuccess() {
// After successful login, user should be redirected away from /sign page
await expect(this.page).not.toHaveURL(/\/sign/);
}
/**
* Check if login failed (still on sign page with error)
*/
async verifyLoginFailure() {
await expect(this.page).toHaveURL(/\/sign/);
}
}

80
pages/ProblemPage.ts Normal file
View File

@@ -0,0 +1,80 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Page Object Model for Problem Detail page
* URL: /pro/{id}
*/
export class ProblemPage {
readonly page: Page;
readonly problemTitle: Locator;
readonly problemContent: Locator;
readonly submitButton: Locator;
readonly timeLimitText: Locator;
readonly memoryLimitText: Locator;
readonly subtaskInfo: Locator;
constructor(page: Page) {
this.page = page;
// Problem detail elements
this.problemTitle = page.locator('h1, h2').first();
this.problemContent = page.locator('#problem-content, .problem-content, iframe');
this.submitButton = page.locator('a:has-text("提交"), a:has-text("Submit"), button:has-text("提交"), button:has-text("Submit")');
this.timeLimitText = page.locator('text=/Time Limit|時間限制/');
this.memoryLimitText = page.locator('text=/Memory Limit|記憶體限制/');
this.subtaskInfo = page.locator('text=/Subtask|子任務/');
}
/**
* Navigate to a specific problem page
*/
async goto(problemId: number) {
await this.page.goto(`/pro/${problemId}`);
await this.page.waitForLoadState('networkidle');
}
/**
* Click the submit button to go to submission page
*/
async goToSubmit() {
await this.submitButton.click();
await this.page.waitForLoadState('networkidle');
}
/**
* Verify we are on the problem page
*/
async verifyOnPage(problemId: number) {
await expect(this.page).toHaveURL(new RegExp(`/pro/${problemId}`));
}
/**
* Check if problem has content loaded
*/
async verifyProblemLoaded() {
// Wait for either the content div or iframe to be visible
await expect(
this.problemContent.or(this.page.locator('iframe'))
).toBeVisible({ timeout: 10000 });
}
/**
* Get problem limits information
*/
async getLimits() {
const limits = {
timeLimit: '',
memoryLimit: '',
};
if (await this.timeLimitText.isVisible()) {
limits.timeLimit = await this.timeLimitText.textContent() || '';
}
if (await this.memoryLimitText.isVisible()) {
limits.memoryLimit = await this.memoryLimitText.textContent() || '';
}
return limits;
}
}

88
pages/ProblemSetPage.ts Normal file
View File

@@ -0,0 +1,88 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Page Object Model for Problem Set page
* URL: /proset
*/
export class ProblemSetPage {
readonly page: Page;
readonly problemLinks: Locator;
readonly searchInput: Locator;
readonly proClassSelect: Locator;
constructor(page: Page) {
this.page = page;
// Problem list elements
this.problemLinks = page.locator('a[href^="/pro/"]');
this.searchInput = page.locator('input[placeholder*="搜尋"], input[placeholder*="Search"]');
this.proClassSelect = page.locator('select');
}
/**
* Navigate to the problem set page
*/
async goto() {
await this.page.goto('/proset');
await this.page.waitForLoadState('networkidle');
}
/**
* Get all visible problems
*/
async getVisibleProblems() {
return await this.problemLinks.all();
}
/**
* Click on a problem by its ID
*/
async clickProblem(problemId: number) {
await this.page.click(`a[href="/pro/${problemId}"]`);
await this.page.waitForLoadState('networkidle');
}
/**
* Search for problems by name
*/
async searchProblem(searchText: string) {
if (await this.searchInput.isVisible()) {
await this.searchInput.fill(searchText);
await this.page.keyboard.press('Enter');
await this.page.waitForLoadState('networkidle');
}
}
/**
* Select a ProClass filter
*/
async selectProClass(proClassName: string) {
if (await this.proClassSelect.isVisible()) {
await this.proClassSelect.selectOption({ label: proClassName });
await this.page.waitForLoadState('networkidle');
}
}
/**
* Verify we are on the problem set page
*/
async verifyOnPage() {
await expect(this.page).toHaveURL(/\/proset/);
}
/**
* Get the first problem ID from the list
*/
async getFirstProblemId(): Promise<number | null> {
const firstProblem = this.problemLinks.first();
if (await firstProblem.count() === 0) {
return null;
}
const href = await firstProblem.getAttribute('href');
if (!href) return null;
const match = href.match(/\/pro\/(\d+)/);
return match ? parseInt(match[1]) : null;
}
}

110
pages/SubmitPage.ts Normal file
View File

@@ -0,0 +1,110 @@
import { Page, Locator, expect } from '@playwright/test';
/**
* Page Object Model for Problem Submission page
* URL: /submit/{id}
*/
export class SubmitPage {
readonly page: Page;
readonly compilerSelect: Locator;
readonly codeTextarea: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// Submit form elements
this.compilerSelect = page.locator('select[name="compiler"], #compiler-select, select').first();
this.codeTextarea = page.locator('textarea[name="code"], textarea#code, .CodeMirror textarea, textarea').first();
this.submitButton = page.locator('button:has-text("提交"), button:has-text("Submit"), input[type="submit"]').first();
this.errorMessage = page.locator('.error, .alert-danger, [class*="error"]');
}
/**
* Navigate to submit page for a specific problem
*/
async goto(problemId: number) {
await this.page.goto(`/submit/${problemId}`);
await this.page.waitForLoadState('networkidle');
}
/**
* Select a compiler
*/
async selectCompiler(compiler: string) {
await this.compilerSelect.selectOption({ label: compiler });
}
/**
* Fill in the code
* Note: This might need adjustment if the site uses CodeMirror or Monaco editor
*/
async fillCode(code: string) {
// Check if CodeMirror is being used
const codeMirror = this.page.locator('.CodeMirror');
if (await codeMirror.count() > 0) {
// CodeMirror editor - need to use execCommand
await codeMirror.click();
await this.page.evaluate((codeText) => {
const cm = (window as any).CodeMirror;
if (cm && cm.instances && cm.instances.length > 0) {
cm.instances[0].setValue(codeText);
}
}, code);
} else {
// Regular textarea
await this.codeTextarea.fill(code);
}
}
/**
* Submit the code
*/
async submit() {
await this.submitButton.click();
// Wait for navigation or response
try {
await this.page.waitForLoadState('networkidle', { timeout: 5000 });
} catch (e) {
// Ignore timeout if page doesn't navigate
}
}
/**
* Submit code with compiler selection
*/
async submitCode(compiler: string, code: string) {
await this.selectCompiler(compiler);
await this.fillCode(code);
await this.submit();
}
/**
* Verify we are on the submit page
*/
async verifyOnPage(problemId: number) {
await expect(this.page).toHaveURL(new RegExp(`/submit/${problemId}`));
}
/**
* Check if there's an error message
*/
async getErrorMessage(): Promise<string | null> {
if (await this.errorMessage.isVisible()) {
return await this.errorMessage.textContent();
}
return null;
}
/**
* Wait for submission cooldown message to disappear
*/
async waitForCooldown(timeoutMs: number = 31000) {
const cooldownMessage = this.page.locator('text=/冷卻|cooldown|請稍候/i');
if (await cooldownMessage.isVisible()) {
await cooldownMessage.waitFor({ state: 'hidden', timeout: timeoutMs });
}
}
}