From b8130f4b0bf8ef80ee0eb20c06cbf77b4d9ea1f6 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Thu, 24 Apr 2025 10:02:33 +0100 Subject: [PATCH] test(*): Add POM to BDD tests --- tests/bdd/pages/HomePage.ts | 12 +++ tests/bdd/pages/ResultsPage.ts | 91 +++++++++++++++++++ tests/bdd/pages/UploadPage.ts | 37 ++++++++ tests/bdd/steps/agentic_progress.steps.ts | 9 +- tests/bdd/steps/assessment_history.steps.ts | 17 ++-- tests/bdd/steps/assessment_steps.ts | 9 +- .../bdd/steps/assessment_submission.steps.ts | 65 ++++++------- tests/bdd/support/world.ts | 10 ++ tsconfig.json | 1 + 9 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 tests/bdd/pages/HomePage.ts create mode 100644 tests/bdd/pages/ResultsPage.ts create mode 100644 tests/bdd/pages/UploadPage.ts diff --git a/tests/bdd/pages/HomePage.ts b/tests/bdd/pages/HomePage.ts new file mode 100644 index 0000000..b66ed3e --- /dev/null +++ b/tests/bdd/pages/HomePage.ts @@ -0,0 +1,12 @@ +import type { Page } from '@playwright/test'; + +export class HomePage { + readonly page: Page; + constructor(page: Page) { + this.page = page; + } + + async navigateTo() { + await this.page.goto('http://localhost:5173/'); + } +} diff --git a/tests/bdd/pages/ResultsPage.ts b/tests/bdd/pages/ResultsPage.ts new file mode 100644 index 0000000..c8dce2c --- /dev/null +++ b/tests/bdd/pages/ResultsPage.ts @@ -0,0 +1,91 @@ +import type { Page } from '@playwright/test'; + +export class ResultsPage { + readonly page: Page; + constructor(page: Page) { + this.page = page; + } + + headingSelector = '[data-testid="results-heading"]'; + feedbackSelector = '[data-testid="feedback"]'; + gradeSelector = '[data-testid="feedback"] div:has(span:has-text("Grade:"))'; + strengthsSelector = '[data-testid="feedback"] div:has(span:has-text("Strengths:"))'; + areasForImprovementSelector = '[data-testid="feedback"] div:has(span:has-text("Areas for Improvement:"))'; + individualizedActivitySelector = '[data-testid="feedback"] div:has(span:has-text("Individualized Activity:"))'; + reflectionQuestionSelector = '[data-testid="feedback"] div:has(span:has-text("Reflection Question:"))'; + teacherSuggestionSelector = '[data-testid="feedback"] div:has(span:has-text("Teacher Suggestion:"))'; + spellingAndGrammarSelector = '[data-testid="feedback"] div:has(span:has-text("Spelling and Grammar:"))'; + reasoningSummarySelector = '[data-testid="feedback"] details > summary'; + reasoningTextSelector = '[data-testid="feedback"] details > div'; + backToUploadSelector = 'a[aria-label="Back to upload page"]'; + noResultsSelector = 'div[aria-live="polite"][role="status"]'; + clearHistorySelector = 'button[aria-label="Clear all assessment history"]'; + + async navigateTo() { + await this.page.goto('http://localhost:5173/results'); + } + + async getHeadingText() { + return this.page.textContent(this.headingSelector); + } + + async waitForHeadingVisible(timeout = 60000) { + await this.page.waitForSelector(this.headingSelector, { state: 'visible', timeout }); + } + + async isFeedbackVisible() { + return this.page.isVisible(this.feedbackSelector); + } + + async waitForFeedbackVisible(timeout = 60000) { + await this.page.waitForSelector(this.feedbackSelector, { state: 'visible', timeout }); + } + + async getGrade() { + return this.page.textContent(this.gradeSelector); + } + + async getStrengths() { + return this.page.textContent(this.strengthsSelector); + } + + async getAreasForImprovement() { + return this.page.textContent(this.areasForImprovementSelector); + } + + async getIndividualizedActivity() { + return this.page.textContent(this.individualizedActivitySelector); + } + + async getReflectionQuestion() { + return this.page.textContent(this.reflectionQuestionSelector); + } + + async getTeacherSuggestion() { + return this.page.textContent(this.teacherSuggestionSelector); + } + + async getSpellingAndGrammar() { + return this.page.textContent(this.spellingAndGrammarSelector); + } + + async expandReasoning() { + await this.page.click(this.reasoningSummarySelector); + } + + async getReasoning() { + return this.page.textContent(this.reasoningTextSelector); + } + + async clickBackToUpload() { + await this.page.click(this.backToUploadSelector); + } + + async isNoResultsMessageVisible() { + return this.page.isVisible(this.noResultsSelector); + } + + async clearHistory() { + await this.page.click(this.clearHistorySelector); + } +} diff --git a/tests/bdd/pages/UploadPage.ts b/tests/bdd/pages/UploadPage.ts new file mode 100644 index 0000000..4ec7f3c --- /dev/null +++ b/tests/bdd/pages/UploadPage.ts @@ -0,0 +1,37 @@ +import type { Page } from '@playwright/test'; + +export class UploadPage { + readonly page: Page; + constructor(page: Page) { + this.page = page; + } + + taskInputSelector = '[data-testid="task-input"]'; + fileInputSelector = 'input[type="file"]'; + submissionInputSelector = '[data-testid="submission-input"]'; + submitButtonSelector = '[data-testid="submit-button"]'; + + async navigateTo() { + await this.page.goto('http://localhost:5173/upload'); + } + + async setTaskDescription(text: string) { + await this.page.fill(this.taskInputSelector, text); + } + + async uploadFile(filePath: string) { + await this.page.setInputFiles(this.fileInputSelector, filePath); + } + + async setSubmissionText(text: string) { + await this.page.fill(this.submissionInputSelector, text); + } + + async submit() { + await this.page.click(this.submitButtonSelector); + } + + async isSubmitButtonEnabled() { + return this.page.isEnabled(this.submitButtonSelector); + } +} diff --git a/tests/bdd/steps/agentic_progress.steps.ts b/tests/bdd/steps/agentic_progress.steps.ts index 5ce906d..54f55ef 100644 --- a/tests/bdd/steps/agentic_progress.steps.ts +++ b/tests/bdd/steps/agentic_progress.steps.ts @@ -1,11 +1,12 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; +import { UploadPage } from '../pages/UploadPage.ts'; Given('I have submitted an assignment for assessment', async function () { - await this.page.goto('http://localhost:5173/upload'); - await this.page.fill('textarea#task', 'Write a short essay about climate change.'); - await this.page.fill('textarea#textInput', 'Climate change is a serious global issue that affects us all.'); - await this.page.click('button[type="submit"]'); + await this.uploadPage.navigateTo(); + await this.uploadPage.setTaskDescription('Write a short essay about climate change.'); + await this.uploadPage.setSubmissionText('Climate change is a serious global issue that affects us all.'); + await this.uploadPage.submit(); }); When('the AI agent is processing my submission', async function () { diff --git a/tests/bdd/steps/assessment_history.steps.ts b/tests/bdd/steps/assessment_history.steps.ts index 532b839..7de321a 100644 --- a/tests/bdd/steps/assessment_history.steps.ts +++ b/tests/bdd/steps/assessment_history.steps.ts @@ -1,8 +1,9 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; +import { ResultsPage } from '../pages/ResultsPage.ts'; Given('I am viewing the assessment history', async function () { - await this.page.goto('http://localhost:5173/results'); + await this.resultsPage.navigateTo(); await this.page.waitForSelector('table[aria-label="Assessment history table"]'); }); @@ -18,13 +19,11 @@ Given('I have previously submitted assessments', async function () { submission: 'Dog', } ]) { - await this.page.goto('http://localhost:5173/upload'); - await this.page.fill('[data-testid="task-input"]', assessment.task); - await this.page.fill('[data-testid="submission-input"]', assessment.submission); - await Promise.all([ - this.page.waitForNavigation({ url: '**/results' }), - this.page.click('[data-testid="submit-button"]') - ]); + await this.uploadPage.navigateTo(); + await this.uploadPage.setTaskDescription(assessment.task); + await this.uploadPage.setSubmissionText(assessment.submission); + await this.uploadPage.submit(); + await this.resultsPage.waitForHeadingVisible(); } }); @@ -66,7 +65,7 @@ Then('that assessment should be removed from the history', async function () { When('I clear all assessment history', async function () { await Promise.all([ this.page.waitForEvent('dialog').then(dialog => dialog.accept()), - this.page.click('button[aria-label="Clear all assessment history"]') + this.resultsPage.clearHistory() ]); }); diff --git a/tests/bdd/steps/assessment_steps.ts b/tests/bdd/steps/assessment_steps.ts index 7b7bf8d..e5dacae 100644 --- a/tests/bdd/steps/assessment_steps.ts +++ b/tests/bdd/steps/assessment_steps.ts @@ -1,9 +1,6 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; - -Given('I am on the homepage', async function () { - await this.page.goto('http://localhost:5173/'); -}); +import { ResultsPage } from '../pages/ResultsPage.ts'; When('I fill in the assessment form and submit', async function () { await this.page.fill('textarea[name="assessment"]', 'Sample student submission'); @@ -11,7 +8,7 @@ When('I fill in the assessment form and submit', async function () { }); Then('I should see my feedback displayed', async function () { - await this.page.waitForSelector('.feedback'); - const feedback = await this.page.textContent('.feedback'); + await this.resultsPage.waitForFeedbackVisible(); + const feedback = await this.resultsPage.getGrade(); expect(feedback).toBeTruthy(); }); diff --git a/tests/bdd/steps/assessment_submission.steps.ts b/tests/bdd/steps/assessment_submission.steps.ts index 1a2077b..c6f5e97 100644 --- a/tests/bdd/steps/assessment_submission.steps.ts +++ b/tests/bdd/steps/assessment_submission.steps.ts @@ -1,69 +1,72 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; - -async function getFeedbackText(page: any) { - await page.waitForSelector('[data-testid="feedback"]', { state: 'visible' }); - return page.textContent('[data-testid="feedback"]'); -} +import { UploadPage } from '../pages/UploadPage.ts'; +import { ResultsPage } from '../pages/ResultsPage.ts'; Given('I am on the "Upload Student Submissions" page', async function () { - await this.page.goto('http://localhost:5173/upload'); + await this.uploadPage.navigateTo(); }); When('I enter a task description', async function () { - await this.page.waitForSelector('[data-testid="task-input"]', { state: 'visible' }); - await this.page.fill('[data-testid="task-input"]', 'Write a short essay about climate change.'); + await this.uploadPage.setTaskDescription('Write a short essay about climate change.'); }); When('I enter a student submission', async function () { - await this.page.waitForSelector('[data-testid="submission-input"]', { state: 'visible' }); - await this.page.fill('[data-testid="submission-input"]', 'Climate change is a serious global issue that affects us all.'); + await this.uploadPage.setSubmissionText('Climate change is a serious global issue that affects us all.'); }); When('I submit the assessment form', async function () { - await this.page.waitForSelector('[data-testid="submit-button"]', { state: 'visible' }); - await this.page.click('[data-testid="submit-button"]'); + await this.uploadPage.submit(); }); When('I leave the submission field empty', async function () { - await this.page.waitForSelector('[data-testid="submission-input"]', { state: 'visible' }); - await this.page.fill('[data-testid="submission-input"]', ''); + await this.uploadPage.setSubmissionText(''); }); Then('I should be redirected to the "Assessment Results" page', async function () { - // Wait for a unique selector on the results page, increase timeout for slow AI assessment - await this.page.waitForSelector('[data-testid="results-heading"]', { timeout: 60000 }); - const heading = await this.page.textContent('[data-testid="results-heading"]'); + // Increase timeout for slow AI assessment + await this.page.waitForSelector(this.resultsPage.headingSelector, { timeout: 60000 }); + const heading = await this.resultsPage.getHeadingText(); expect(heading).toMatch(/Assessment Results/); }); Then('I should see the AI-generated grade', async function () { - const text = await getFeedbackText(this.page); - expect(text).toMatch(/Grade:/i); + await this.resultsPage.waitForFeedbackVisible(); + const gradeText = await this.resultsPage.getGrade(); + expect(gradeText).toMatch(/Grade:/i); }); Then('I should see strengths, areas for improvement, and individualized activity', async function () { - const text = await getFeedbackText(this.page); - expect(text).toMatch(/Strengths:/i); - expect(text).toMatch(/Areas for Improvement:/i); - expect(text).toMatch(/Individualized Activity:/i); + await this.resultsPage.waitForFeedbackVisible(); + const strengths = await this.resultsPage.getStrengths(); + const areas = await this.resultsPage.getAreasForImprovement(); + const activity = await this.resultsPage.getIndividualizedActivity(); + expect(strengths).toBeTruthy(); + expect(areas).toBeTruthy(); + expect(activity).toBeTruthy(); }); Then('I should see a reflection question and teacher suggestion', async function () { - const text = await getFeedbackText(this.page); - expect(text).toMatch(/Reflection Question:/i); - expect(text).toMatch(/Teacher Suggestion:/i); + this.resultsPage = new ResultsPage(this.page); + await this.resultsPage.waitForFeedbackVisible(); + const reflection = await this.resultsPage.getReflectionQuestion(); + const suggestion = await this.resultsPage.getTeacherSuggestion(); + expect(reflection).toBeTruthy(); + expect(suggestion).toBeTruthy(); }); Then('I should see spelling and grammar feedback', async function () { - const text = await getFeedbackText(this.page); - expect(text).toMatch(/Spelling and Grammar:/i); + this.resultsPage = new ResultsPage(this.page); + await this.resultsPage.waitForFeedbackVisible(); + const spelling = await this.resultsPage.getSpellingAndGrammar(); + expect(spelling).toBeTruthy(); }); Then('I should be able to expand to view AI reasoning', async function () { - await this.page.click('summary:has-text("Show AI Reasoning")'); - const text = await getFeedbackText(this.page); - expect(text).toMatch(/reasoning/i); + this.resultsPage = new ResultsPage(this.page); + await this.resultsPage.expandReasoning(); + const reasoning = await this.resultsPage.getReasoning(); + expect(reasoning).toBeTruthy(); }); Then('the submit button should be disabled', async function () { diff --git a/tests/bdd/support/world.ts b/tests/bdd/support/world.ts index 6ae14bc..36bb32d 100644 --- a/tests/bdd/support/world.ts +++ b/tests/bdd/support/world.ts @@ -6,14 +6,24 @@ setDefaultTimeout(60000); // 60 seconds let browser: Browser; +import { HomePage } from '../pages/HomePage.ts'; +import { UploadPage } from '../pages/UploadPage.ts'; +import { ResultsPage } from '../pages/ResultsPage.ts'; + class CustomWorld extends World { page!: Page; + homePage!: HomePage; + uploadPage!: UploadPage; + resultsPage!: ResultsPage; async openPage() { if (!browser) { browser = await chromium.launch({ headless: true }); } this.page = await browser.newPage(); + this.homePage = new HomePage(this.page); + this.uploadPage = new UploadPage(this.page); + this.resultsPage = new ResultsPage(this.page); } async closePage() { diff --git a/tsconfig.json b/tsconfig.json index 0b2d886..c84a291 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { + "allowImportingTsExtensions": true, "allowJs": true, "checkJs": true, "esModuleInterop": true,