Initialize
This commit is contained in:
90
pages/ChallengeListPage.ts
Normal file
90
pages/ChallengeListPage.ts
Normal 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
142
pages/ChallengePage.ts
Normal 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
81
pages/LoginPage.ts
Normal 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
80
pages/ProblemPage.ts
Normal 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
88
pages/ProblemSetPage.ts
Normal 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
110
pages/SubmitPage.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user