Skip to content

Commit c4428a2

Browse files
authored
Revert CORS and add end-to-end tests (#32)
* Revert CORS and add end-to-end tests * TEST: Break end-to-end tests * Revert "TEST: Break end-to-end tests" This reverts commit a00ee14.
1 parent 780c3c7 commit c4428a2

10 files changed

Lines changed: 395 additions & 20 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,37 @@ jobs:
7979
run: npm ci
8080
- name: Run Tests
8181
run: npm test
82+
83+
test-e2e:
84+
name: "Test End-to-End"
85+
runs-on: ubuntu-latest
86+
timeout-minutes: 15
87+
defaults:
88+
run:
89+
working-directory: TodoListClient
90+
steps:
91+
- name: Git checkout
92+
uses: actions/checkout@v7
93+
with:
94+
persist-credentials: false
95+
- name: Setup PostgreSQL
96+
uses: ikalnytskyi/action-setup-postgres@v8
97+
with:
98+
username: postgres
99+
password: postgres
100+
- name: Setup .NET
101+
uses: actions/setup-dotnet@v6
102+
with:
103+
dotnet-version: "10.0.*"
104+
- name: Install Node
105+
uses: actions/setup-node@v7
106+
with:
107+
node-version: 22.x
108+
cache: npm
109+
cache-dependency-path: TodoListClient/package-lock.json
110+
- name: Install Dependencies
111+
run: npm ci
112+
- name: Install Playwright Browsers
113+
run: npx playwright install --with-deps chromium
114+
- name: Run End-to-End Tests
115+
run: npm run test:e2e

‎README.md‎

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,24 @@ When the API runs for the first time, it automatically creates the PostgreSQL da
4242

4343
### 1. Start the Database
4444

45-
The application expects a PostgreSQL instance with the connection details defined in `TodoListAPI/appsettings.json`. The easiest way to start one is using Docker:
45+
The application expects a PostgreSQL instance with the connection details defined in `TodoListAPI/appsettings.json`. You can start it using Docker Compose:
4646

4747
```shell
48-
docker run --name TodoListSampleDb -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=TodoList -p 5432:5432 -d postgres
48+
docker compose up --detach --wait
4949
```
5050

51+
- This starts a PostgreSQL database container and a **pgAdmin** container (accessible at [http://localhost:5050](http://localhost:5050), preconfigured to automatically connect to the database).
52+
- To stop the containers:
53+
```shell
54+
docker compose down
55+
```
56+
- To stop the containers and remove persistent data (reset the database):
57+
```shell
58+
docker compose down --volumes
59+
```
60+
5161
> [!NOTE]
52-
> The API will automatically create database tables and seed the demo users and todo-items on its first run. If you ever want a fresh database, stop and remove the container.
62+
> The API will automatically create database tables and seed the demo users and todo-items on its first run.
5363
5464
### 2. Start the API
5565

@@ -127,6 +137,39 @@ From the `TodoListClient` directory:
127137

128138
---
129139

140+
## Running End-to-End (E2E) Tests
141+
142+
The repository includes an end-to-end test suite built with [Playwright](https://playwright.dev/) that validates the entire stack against the real PostgreSQL database, ASP.NET Core API server, and Ember.js client app.
143+
144+
### Running E2E Tests Locally (using Docker)
145+
146+
1. **Start the database in Docker:**
147+
```shell
148+
docker compose up --detach --wait
149+
```
150+
151+
2. **From the `TodoListClient` directory, install browsers (one-time setup):**
152+
```shell
153+
cd TodoListClient
154+
npx playwright install chromium
155+
```
156+
157+
3. **Run the tests:**
158+
```shell
159+
npm run test:e2e
160+
```
161+
162+
> [!TIP]
163+
> **Automatic Server Management:** If the backend API (`dotnet run`) and frontend client (`npm start`) are already running, Playwright reuses them automatically. If they are not running, Playwright launches them in the background, runs the test suite, and shuts them down upon completion.
164+
165+
You can also run tests interactively with the Playwright UI runner:
166+
167+
```shell
168+
npx playwright test --ui
169+
```
170+
171+
---
172+
130173
## Updating Ember.js
131174

132175
The client project uses standard npm packaging and is configured with Ember's modern Vite blueprint (`@ember/app-blueprint`). To upgrade Ember dependencies in the future, use `npx ember-cli-update`:
@@ -148,18 +191,3 @@ The client project uses standard npm packaging and is configured with Ember's mo
148191
```shell
149192
npm test
150193
```
151-
152-
---
153-
154-
## Manual Verification Checklist
155-
156-
When verifying changes or following an upgrade, check that:
157-
158-
- [ ] Application loads at [http://localhost:4200](http://localhost:4200) and displays the sign-in form.
159-
- [ ] Attempting to log in with invalid credentials displays an `"Authentication failed"` alert.
160-
- [ ] Logging in as `guest` (`Guest1!`) displays the todo list containing `"owned-by-guest"`.
161-
- [ ] Logging in as `john` (`P@ssw0rd!`) displays the todo list containing `"owned-by-john"`.
162-
- [ ] Input validation: Attempting to save a todo-item with fewer than 4 characters displays a validation error.
163-
- [ ] Adding a valid todo-item saves successfully and navigates back to the updated list.
164-
- [ ] Clicking **Logout** invalidates the session and returns to the login screen.
165-
- [ ] Navigating directly to a protected route (e.g. [http://localhost:4200/s/todo-items](http://localhost:4200/s/todo-items)) while logged out redirects to the login screen.

‎TodoListAPI/Program.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
{
106106
app.UseCors(policy =>
107107
{
108-
policy.WithOrigins("http://localhost:5000").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
108+
policy.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
109109
});
110110
}
111111

‎TodoListClient/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
/npm-debug.log*
2020
/testem.log
2121
/yarn-error.log
22+
/test-results/
23+
/playwright-report/
2224

2325
# ember-try
2426
/.node_modules.ember-try/

‎TodoListClient/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ From the `TodoListClient` directory:
4242
```
4343
- Run tests interactively in the browser:
4444
Start the dev server (`npm start`) and navigate to [http://localhost:4200/tests](http://localhost:4200/tests).
45+
- Run end-to-end tests against the real API (requires PostgreSQL running in Docker):
46+
```shell
47+
npm run test:e2e
48+
```
4549

4650
## Linting & Formatting
4751

‎TodoListClient/e2e/app.spec.js‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Todo List Application', () => {
4+
test('Application loads at http://localhost:4200 and displays the sign-in form', async ({
5+
page,
6+
}) => {
7+
await page.goto('/');
8+
await expect(page).toHaveURL(/\/login/);
9+
await expect(page.locator('h1')).toHaveText('Please sign in');
10+
await expect(page.locator('[data-test-username]')).toBeVisible();
11+
await expect(page.locator('[data-test-password]')).toBeVisible();
12+
await expect(page.locator('[data-test-submit]')).toBeVisible();
13+
});
14+
15+
test('Attempting to log in with invalid credentials displays an "Authentication failed" alert', async ({
16+
page,
17+
}) => {
18+
await page.goto('/login');
19+
await page.locator('[data-test-username]').fill('wrong-user');
20+
await page.locator('[data-test-password]').fill('wrong-password');
21+
await page.locator('[data-test-submit]').click();
22+
23+
const alertMessage = page.locator('.ember-notify .message');
24+
await expect(alertMessage).toBeVisible();
25+
await expect(alertMessage).toHaveText('Authentication failed');
26+
await expect(page).toHaveURL(/\/login/);
27+
});
28+
29+
test('Logging in as guest displays the todo list containing "owned-by-guest"', async ({
30+
page,
31+
}) => {
32+
await page.goto('/login');
33+
await page.locator('[data-test-username]').fill('guest');
34+
await page.locator('[data-test-password]').fill('Guest1!');
35+
await page.locator('[data-test-submit]').click();
36+
37+
await expect(page).toHaveURL(/\/s\/todo-items/);
38+
await expect(page.locator('h1')).toHaveText('Todo Items');
39+
await expect(page.locator('[data-test-description]')).toContainText([
40+
'owned-by-guest',
41+
]);
42+
await expect(page.locator('tbody')).not.toContainText('owned-by-john');
43+
});
44+
45+
test('Logging in as john displays the todo list containing "owned-by-john"', async ({
46+
page,
47+
}) => {
48+
await page.goto('/login');
49+
await page.locator('[data-test-username]').fill('john');
50+
await page.locator('[data-test-password]').fill('P@ssw0rd!');
51+
await page.locator('[data-test-submit]').click();
52+
53+
await expect(page).toHaveURL(/\/s\/todo-items/);
54+
await expect(page.locator('h1')).toHaveText('Todo Items');
55+
await expect(page.locator('[data-test-description]')).toContainText([
56+
'owned-by-john',
57+
]);
58+
await expect(page.locator('tbody')).not.toContainText('owned-by-guest');
59+
});
60+
61+
test('Input validation: Attempting to save a todo-item with fewer than 4 characters displays a validation error', async ({
62+
page,
63+
}) => {
64+
await page.goto('/login');
65+
await page.locator('[data-test-username]').fill('guest');
66+
await page.locator('[data-test-password]').fill('Guest1!');
67+
await page.locator('[data-test-submit]').click();
68+
await expect(page).toHaveURL(/\/s\/todo-items/);
69+
70+
await page.locator('[data-test-add]').click();
71+
await expect(page).toHaveURL(/\/s\/todo-items\/add/);
72+
73+
// Empty description validation
74+
await page.locator('[data-test-submit]').click();
75+
let alertMessage = page.locator('.ember-notify .message');
76+
await expect(alertMessage).toBeVisible();
77+
await expect(alertMessage).toContainText("Description can't be blank");
78+
79+
// Short description (< 4 characters) validation
80+
await page.locator('[data-test-description-input]').fill('abc');
81+
await page.locator('[data-test-submit]').click();
82+
alertMessage = page.locator('.ember-notify .message').last();
83+
await expect(alertMessage).toBeVisible();
84+
await expect(alertMessage).toContainText(
85+
'Description is too short (minimum is 4 characters)',
86+
);
87+
await expect(page).toHaveURL(/\/s\/todo-items\/add/);
88+
});
89+
90+
test('Adding a valid todo-item saves successfully and navigates back to the updated list', async ({
91+
page,
92+
}) => {
93+
await page.goto('/login');
94+
await page.locator('[data-test-username]').fill('guest');
95+
await page.locator('[data-test-password]').fill('Guest1!');
96+
await page.locator('[data-test-submit]').click();
97+
await expect(page).toHaveURL(/\/s\/todo-items/);
98+
99+
await page.locator('[data-test-add]').click();
100+
await expect(page).toHaveURL(/\/s\/todo-items\/add/);
101+
102+
const newDescription = `valid-todo-item-${Date.now()}`;
103+
await page.locator('[data-test-description-input]').fill(newDescription);
104+
await page.locator('[data-test-submit]').click();
105+
106+
await expect(page).toHaveURL(/\/s\/todo-items/);
107+
await expect(page.locator('[data-test-description]')).toContainText([
108+
newDescription,
109+
]);
110+
});
111+
112+
test('Clicking Logout invalidates the session and returns to the login screen', async ({
113+
page,
114+
}) => {
115+
await page.goto('/login');
116+
await page.locator('[data-test-username]').fill('guest');
117+
await page.locator('[data-test-password]').fill('Guest1!');
118+
await page.locator('[data-test-submit]').click();
119+
await expect(page).toHaveURL(/\/s\/todo-items/);
120+
121+
await page.locator('[data-test-logout]').click();
122+
await expect(page).toHaveURL(/\/login/);
123+
await expect(page.locator('h1')).toHaveText('Please sign in');
124+
});
125+
126+
test('Navigating directly to a protected route while logged out redirects to the login screen', async ({
127+
page,
128+
}) => {
129+
await page.goto('/s/todo-items');
130+
await expect(page).toHaveURL(/\/login/);
131+
await expect(page.locator('h1')).toHaveText('Please sign in');
132+
133+
await page.goto('/s/todo-items/add');
134+
await expect(page).toHaveURL(/\/login/);
135+
await expect(page.locator('h1')).toHaveText('Please sign in');
136+
});
137+
});

‎TodoListClient/package-lock.json‎

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎TodoListClient/package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"lint:js:fix": "eslint . --fix",
2525
"postinstall": "patch-package",
2626
"start": "vite",
27-
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto",
27+
"test": "concurrently \"npm:lint\" \"npm:test:ember\" --names \"lint,test:\" --prefixColors auto",
28+
"test:e2e": "playwright test",
2829
"test:ember": "vite build --mode development && ember test --path dist"
2930
},
3031
"devDependencies": {
@@ -44,6 +45,7 @@
4445
"@eslint/js": "^9.25.0",
4546
"@glimmer/component": "^2.0.0",
4647
"@glimmer/tracking": "^1.1.2",
48+
"@playwright/test": "^1.63.0",
4749
"@rollup/plugin-babel": "^7.0.0",
4850
"@warp-drive/build-config": "^0.0.3",
4951
"babel-plugin-ember-template-compilation": "^3.1.0",

0 commit comments

Comments
 (0)