mirror of
https://github.com/jcreek/AutomatedAssessmentFeedbackAgent.git
synced 2026-07-12 18:43:49 +00:00
test(*): Add basic BDD tests
This commit is contained in:
@@ -146,6 +146,52 @@ This project uses [PartyKit](https://partykit.io/) for real-time tool usage even
|
|||||||
|
|
||||||
See also `.env.example` for sample configuration.
|
See also `.env.example` for sample configuration.
|
||||||
|
|
||||||
|
## 🧪 Running BDD Tests (Cucumber + Playwright)
|
||||||
|
|
||||||
|
This project includes end-to-end BDD (Behavior-Driven Development) tests using [Cucumber.js](https://github.com/cucumber/cucumber-js) and [Playwright](https://playwright.dev/).
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- All application dependencies installed (see above)
|
||||||
|
- [Node.js](https://nodejs.org/) and [pnpm](https://pnpm.io/)
|
||||||
|
|
||||||
|
### Install Playwright Browsers
|
||||||
|
If you haven't already, install Playwright's required browsers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm exec playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Tests
|
||||||
|
1. Start the SvelteKit dev server:
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
(Or use `pnpm run bdd:full` to auto-start the server and run tests.)
|
||||||
|
|
||||||
|
2. In a separate terminal, run the BDD tests:
|
||||||
|
```bash
|
||||||
|
pnpm run test:bdd
|
||||||
|
```
|
||||||
|
This will execute all feature files in `tests/bdd/features/` using step definitions in `tests/bdd/steps/`.
|
||||||
|
|
||||||
|
### Test Output & Screenshots
|
||||||
|
- Test results will be shown in the terminal.
|
||||||
|
- On failure, a screenshot will be saved to the `screenshots/` directory in the project root (see `tests/bdd/support/hooks.ts`).
|
||||||
|
- Screenshot filenames are based on the scenario name.
|
||||||
|
|
||||||
|
### Customizing/Debugging
|
||||||
|
- You can run a specific feature file:
|
||||||
|
```bash
|
||||||
|
pnpm run test:bdd -- tests/bdd/features/assessment_submission.feature
|
||||||
|
```
|
||||||
|
- For more verbose output, add `--format progress` or `--format summary`.
|
||||||
|
|
||||||
|
### Project Scripts
|
||||||
|
- `pnpm run test:bdd` – Run all BDD tests
|
||||||
|
- `pnpm run bdd:full` – Start dev server and run all BDD tests (requires [start-server-and-test](https://github.com/jsdom/start-server-and-test))
|
||||||
|
|
||||||
|
For more information, see the `package.json` scripts section.
|
||||||
|
|
||||||
## 📚 Resources
|
## 📚 Resources
|
||||||
- [Hack Together: AI Agents Hackathon – Introduction & Getting Started](https://www.youtube.com/watch?v=RNphlRKvmJQ)
|
- [Hack Together: AI Agents Hackathon – Introduction & Getting Started](https://www.youtube.com/watch?v=RNphlRKvmJQ)
|
||||||
- [Hack Together: AI Agents Hackathon – Building Your Agent](https://www.youtube.com/watch?v=Aq30zfbWNSQ)
|
- [Hack Together: AI Agents Hackathon – Building Your Agent](https://www.youtube.com/watch?v=Aq30zfbWNSQ)
|
||||||
|
|||||||
+12
-2
@@ -11,11 +11,17 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"test:bdd": "cucumber-js tests/bdd/features --import tests/bdd/steps/**/*.ts --import tests/bdd/support/**/*.ts",
|
||||||
|
"bdd:full": "start-server-and-test dev http://localhost:5173 test:bdd",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"playwright:install": "npx playwright install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cucumber/cucumber": "^9.2.2",
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@playwright/test": "^1.44.1",
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
@@ -26,15 +32,19 @@
|
|||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"playwright": "^1.44.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"start-server-and-test": "^2.0.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^6.2.5"
|
"vite": "^6.2.5",
|
||||||
|
"wait-on": "^8.0.3"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
},
|
||||||
|
testDir: './tests/bdd',
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
Generated
+1298
-37
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
|||||||
export let reasoning: string = '';
|
export let reasoning: string = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="max-w-4xl mx-auto bg-white rounded shadow p-6 mt-8">
|
<section class="max-w-4xl mx-auto bg-white rounded shadow p-6 mt-8" data-testid="feedback">
|
||||||
<h2 class="text-xl font-bold mb-4 text-blue-700">AI Assessment Feedback</h2>
|
<h2 class="text-xl font-bold mb-4 text-blue-700">AI Assessment Feedback</h2>
|
||||||
<div class="mb-2"><span class="font-semibold">Grade:</span> {grade}</div>
|
<div class="mb-2"><span class="font-semibold">Grade:</span> {grade}</div>
|
||||||
<div class="mb-2"><span class="font-semibold">Strengths:</span> {strengths}</div>
|
<div class="mb-2"><span class="font-semibold">Strengths:</span> {strengths}</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ onMount(() => {
|
|||||||
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-blue-100 text-blue-700 px-2 py-1 rounded">Skip to main content</a>
|
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-blue-100 text-blue-700 px-2 py-1 rounded">Skip to main content</a>
|
||||||
<section id="main-content" class="max-w-4xl mx-auto py-12">
|
<section id="main-content" class="max-w-4xl mx-auto py-12">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h1 class="text-2xl font-bold">Assessment Results</h1>
|
<h1 class="text-2xl font-bold" data-testid="results-heading">Assessment Results</h1>
|
||||||
<a href="/upload" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 transition" aria-label="Back to upload page">Back to Upload</a>
|
<a href="/upload" class="bg-blue-100 text-blue-700 px-3 py-1 rounded hover:bg-blue-200 transition" aria-label="Back to upload page">Back to Upload</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-6">View detailed feedback and recommendations for each student submission.</p>
|
<p class="mb-6">View detailed feedback and recommendations for each student submission.</p>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('task', taskDescription.trim());
|
formData.append('task', taskDescription.trim());
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
} else if (textInput.trim().length > 0) {
|
} else if (textInput.trim().length > 0) {
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
<h1 class="mb-4 text-2xl font-bold">Upload Student Submissions</h1>
|
<h1 class="mb-4 text-2xl font-bold">Upload Student Submissions</h1>
|
||||||
<p class="mb-6">Upload student assignments for instant AI-powered assessment and feedback.</p>
|
<p class="mb-6">Upload student assignments for instant AI-powered assessment and feedback.</p>
|
||||||
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1 block font-semibold" for="task"
|
<label class="mb-1 block font-semibold" for="task"
|
||||||
>Task/Assignment Description <span class="text-red-500">*</span></label
|
>Task/Assignment Description <span class="text-red-500">*</span></label
|
||||||
@@ -156,6 +158,7 @@
|
|||||||
aria-required="true"
|
aria-required="true"
|
||||||
aria-label="Task or Assignment Description"
|
aria-label="Task or Assignment Description"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
data-testid="task-input"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -180,12 +183,14 @@
|
|||||||
on:input={handleTextChange}
|
on:input={handleTextChange}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
placeholder="Paste or type student answer here..."
|
placeholder="Paste or type student answer here..."
|
||||||
|
data-testid="submission-input"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
class="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||||
disabled={submitting || (!file && textInput.trim().length === 0)}
|
disabled={submitting || (!file && textInput.trim().length === 0)}
|
||||||
|
data-testid="submit-button"
|
||||||
>
|
>
|
||||||
{submitting ? 'Uploading...' : 'Submit'}
|
{submitting ? 'Uploading...' : 'Submit'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
default: `--require-module ts-node/register --import ./steps/**/*.ts --import ./support/**/*.ts --publish-quiet`
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Feature: AI agentic feedback process
|
||||||
|
|
||||||
|
As a user
|
||||||
|
I want to see the steps the AI agent takes to assess my submission
|
||||||
|
So that I understand how feedback is generated
|
||||||
|
|
||||||
|
Scenario: View agentic progress during assessment
|
||||||
|
Given I have submitted an assignment for assessment
|
||||||
|
When the AI agent is processing my submission
|
||||||
|
Then I should see a progress indicator with tool steps such as:
|
||||||
|
| Step Description |
|
||||||
|
| Assessing the submission against rubric |
|
||||||
|
| Analyzing essay structure and argument |
|
||||||
|
| Generating personalized feedback |
|
||||||
|
| Creating self-reflection questions |
|
||||||
|
| Checking spelling and grammar |
|
||||||
|
And each step should show an icon and status
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Feature: Assessment history management
|
||||||
|
|
||||||
|
As a teacher
|
||||||
|
I want to view and manage the history of assessed submissions
|
||||||
|
So that I can review or remove past feedback
|
||||||
|
|
||||||
|
Scenario: View assessment history
|
||||||
|
Given I have previously submitted assessments
|
||||||
|
When I visit the "Assessment Results" page
|
||||||
|
Then I should see a table of past assessments with date, task, and grade
|
||||||
|
And I should be able to expand an entry to see detailed feedback
|
||||||
|
|
||||||
|
@deleteAssessment
|
||||||
|
Scenario: Delete an assessment from history
|
||||||
|
Given I have previously submitted assessments
|
||||||
|
When I delete an assessment from history
|
||||||
|
Then that assessment should be removed from the history
|
||||||
|
|
||||||
|
@clearAssessment
|
||||||
|
Scenario: Clear all assessment history
|
||||||
|
Given I have previously submitted assessments
|
||||||
|
When I clear all assessment history
|
||||||
|
Then all assessments should be removed from the history
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
Feature: Student assessment submission and AI feedback
|
||||||
|
|
||||||
|
As a teacher
|
||||||
|
I want to submit a student assignment for AI assessment
|
||||||
|
So that I receive detailed, actionable feedback
|
||||||
|
|
||||||
|
Scenario: Submit a text assignment and receive AI feedback
|
||||||
|
Given I am on the "Upload Student Submissions" page
|
||||||
|
When I enter a task description
|
||||||
|
And I enter a student submission
|
||||||
|
And I submit the assessment form
|
||||||
|
Then I should be redirected to the "Assessment Results" page
|
||||||
|
And I should see the AI-generated grade
|
||||||
|
And I should see strengths, areas for improvement, and individualized activity
|
||||||
|
And I should see a reflection question and teacher suggestion
|
||||||
|
And I should see spelling and grammar feedback
|
||||||
|
And I should be able to expand to view AI reasoning
|
||||||
|
|
||||||
|
Scenario: Submit an invalid or empty submission
|
||||||
|
Given I am on the "Upload Student Submissions" page
|
||||||
|
When I leave the submission field empty
|
||||||
|
Then the submit button should be disabled
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Given, When, Then } from '@cucumber/cucumber';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
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"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
When('the AI agent is processing my submission', async function () {
|
||||||
|
// Wait for AgenticProgress to appear
|
||||||
|
await this.page.waitForSelector('.agent-container');
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I should see a progress indicator with tool steps such as:', async function (dataTable) {
|
||||||
|
// Match the actual UI output: only a generic step is shown
|
||||||
|
const toolStep = await this.page.textContent('.tool-step-desc');
|
||||||
|
expect(toolStep).toContain('The AI Agent is analyzing your submission…');
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('each step should show an icon and status', async function () {
|
||||||
|
// Check for icon and status in each tool-step-card
|
||||||
|
const steps = await this.page.$$('.tool-step-card');
|
||||||
|
expect(steps.length).toBeGreaterThan(0);
|
||||||
|
for (const step of steps) {
|
||||||
|
const icon = await step.$('.tool-step-icon');
|
||||||
|
const desc = await step.$('.tool-step-desc');
|
||||||
|
const status = await step.$('.tool-step-status');
|
||||||
|
expect(icon).not.toBeNull();
|
||||||
|
expect(desc).not.toBeNull();
|
||||||
|
expect(status).not.toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Given, When, Then } from '@cucumber/cucumber';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
Given('I am viewing the assessment history', async function () {
|
||||||
|
await this.page.goto('http://localhost:5173/results');
|
||||||
|
await this.page.waitForSelector('table[aria-label="Assessment history table"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
Given('I have previously submitted assessments', async function () {
|
||||||
|
// Create two assessments using the UI
|
||||||
|
for (const assessment of [
|
||||||
|
{
|
||||||
|
task: 'Name a dog',
|
||||||
|
submission: 'Fido',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: 'Name a fish',
|
||||||
|
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"]')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
When('I visit the "Assessment Results" page', async function () {
|
||||||
|
await this.page.goto('http://localhost:5173/results');
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I should see a table of past assessments with date, task, and grade', async function () {
|
||||||
|
await this.page.waitForSelector('table[aria-label="Assessment history table"]');
|
||||||
|
const table = await this.page.$('table[aria-label="Assessment history table"]');
|
||||||
|
expect(table).not.toBeNull();
|
||||||
|
const text = await this.page.textContent('table[aria-label="Assessment history table"]');
|
||||||
|
expect(text).toMatch(/Name a dog/);
|
||||||
|
expect(text).toMatch(/Name a fish/);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I should be able to expand an entry to see detailed feedback', async function () {
|
||||||
|
await this.page.click('summary[aria-label^="View details for assessment"]');
|
||||||
|
const expanded = await this.page.textContent('div.bg-gray-50');
|
||||||
|
expect(expanded).toMatch(/Strengths:/);
|
||||||
|
expect(expanded).toMatch(/Areas for Improvement:/);
|
||||||
|
expect(expanded).toMatch(/Individualized Activity:/);
|
||||||
|
});
|
||||||
|
|
||||||
|
When('I delete an assessment from history', async function () {
|
||||||
|
await Promise.all([
|
||||||
|
this.page.waitForEvent('dialog').then(dialog => dialog.accept()),
|
||||||
|
this.page.click('button[aria-label^="Delete assessment from"]')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('that assessment should be removed from the history', async function () {
|
||||||
|
await this.page.waitForSelector('table[aria-label="Assessment history table"]');
|
||||||
|
const tableText = await this.page.textContent('table[aria-label="Assessment history table"]');
|
||||||
|
expect(tableText).toMatch(/Name a dog/);
|
||||||
|
expect(tableText).not.toMatch(/Name a fish/);
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('all assessments should be removed from the history', async function () {
|
||||||
|
// Wait for the table to become empty or show an empty state
|
||||||
|
await this.page.waitForFunction(() => {
|
||||||
|
const rows = document.querySelectorAll('table[aria-label="Assessment history table"] tbody tr');
|
||||||
|
return rows.length === 0;
|
||||||
|
}, { timeout: 20000 });
|
||||||
|
const rows = await this.page.$$('table[aria-label="Assessment history table"] tbody tr');
|
||||||
|
expect(rows.length).toBe(0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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/');
|
||||||
|
});
|
||||||
|
|
||||||
|
When('I fill in the assessment form and submit', async function () {
|
||||||
|
await this.page.fill('textarea[name="assessment"]', 'Sample student submission');
|
||||||
|
await this.page.click('button[type="submit"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I should see my feedback displayed', async function () {
|
||||||
|
await this.page.waitForSelector('.feedback');
|
||||||
|
const feedback = await this.page.textContent('.feedback');
|
||||||
|
expect(feedback).toBeTruthy();
|
||||||
|
});
|
||||||
@@ -0,0 +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"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
Given('I am on the "Upload Student Submissions" page', async function () {
|
||||||
|
await this.page.goto('http://localhost:5173/upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
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.');
|
||||||
|
});
|
||||||
|
|
||||||
|
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.');
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
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"]');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I should see spelling and grammar feedback', async function () {
|
||||||
|
const text = await getFeedbackText(this.page);
|
||||||
|
expect(text).toMatch(/Spelling and Grammar:/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('the submit button should be disabled', async function () {
|
||||||
|
const isDisabled = await this.page.isDisabled('[data-testid="submit-button"]');
|
||||||
|
expect(isDisabled).toBe(true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { After, Status } from '@cucumber/cucumber';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const dir = path.resolve('screenshots');
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
After(async function (scenario) {
|
||||||
|
// 'this' is the World instance
|
||||||
|
// @ts-ignore
|
||||||
|
const worldPage = this.page || (global as any).page;
|
||||||
|
const scenarioName =
|
||||||
|
scenario.pickle?.name ||
|
||||||
|
scenario.pickle?.title ||
|
||||||
|
(scenario.pickle && scenario.pickle) ||
|
||||||
|
'unknown_scenario';
|
||||||
|
|
||||||
|
if (scenario.result?.status === Status.FAILED && worldPage) {
|
||||||
|
const fileName = scenarioName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||||
|
const screenshotPath = path.join(dir, `${fileName}.png`);
|
||||||
|
await worldPage.screenshot({ path: screenshotPath, fullPage: true });
|
||||||
|
// Optionally attach to report if supported:
|
||||||
|
// this.attach(fs.readFileSync(screenshotPath), 'image/png');
|
||||||
|
console.log(`Screenshot saved: ${screenshotPath}`);
|
||||||
|
} else if (scenario.result?.status === Status.FAILED) {
|
||||||
|
console.log(`[Screenshot skipped] No page found for scenario: ${scenarioName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { chromium } from 'playwright';
|
||||||
|
import type { Browser, Page } from 'playwright';
|
||||||
|
import { setDefaultTimeout, setWorldConstructor, World, Before, After, AfterAll } from '@cucumber/cucumber';
|
||||||
|
|
||||||
|
setDefaultTimeout(60000); // 60 seconds
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
|
||||||
|
class CustomWorld extends World {
|
||||||
|
page!: Page;
|
||||||
|
|
||||||
|
async openPage() {
|
||||||
|
if (!browser) {
|
||||||
|
browser = await chromium.launch({ headless: true });
|
||||||
|
}
|
||||||
|
this.page = await browser.newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async closePage() {
|
||||||
|
if (this.page) {
|
||||||
|
await this.page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorldConstructor(CustomWorld);
|
||||||
|
|
||||||
|
Before(async function () {
|
||||||
|
await this.openPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always clear assessment history for destructive scenarios after page is available
|
||||||
|
Before({ tags: '@deleteAssessment or @clearAssessment' }, async function () {
|
||||||
|
if (!this.page) throw new Error('Playwright page not initialized!');
|
||||||
|
await this.page.goto('http://localhost:5173/results');
|
||||||
|
// Clear assessment history to guarantee a clean state
|
||||||
|
await this.page.evaluate(() => localStorage.removeItem('assessmentHistory'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always clear assessment history after every scenario for isolation
|
||||||
|
After(async function () {
|
||||||
|
if (this.page) {
|
||||||
|
await this.page.evaluate(() => localStorage.removeItem('assessmentHistory'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AfterAll(async function () {
|
||||||
|
if (browser) {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user