Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,37 @@ jobs:
run: npm ci
- name: Run Tests
run: npm test

test-e2e:
name: "Test End-to-End"
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: TodoListClient
steps:
- name: Git checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup PostgreSQL
uses: ikalnytskyi/action-setup-postgres@v8
with:
username: postgres
password: postgres
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.*"
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: 22.x
cache: npm
cache-dependency-path: TodoListClient/package-lock.json
- name: Install Dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run End-to-End Tests
run: npm run test:e2e
64 changes: 46 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,24 @@ When the API runs for the first time, it automatically creates the PostgreSQL da

### 1. Start the Database

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

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

- 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).
- To stop the containers:
```shell
docker compose down
```
- To stop the containers and remove persistent data (reset the database):
```shell
docker compose down --volumes
```

> [!NOTE]
> 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.
> The API will automatically create database tables and seed the demo users and todo-items on its first run.

### 2. Start the API

Expand Down Expand Up @@ -127,6 +137,39 @@ From the `TodoListClient` directory:

---

## Running End-to-End (E2E) Tests

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.

### Running E2E Tests Locally (using Docker)

1. **Start the database in Docker:**
```shell
docker compose up --detach --wait
```

2. **From the `TodoListClient` directory, install browsers (one-time setup):**
```shell
cd TodoListClient
npx playwright install chromium
```

3. **Run the tests:**
```shell
npm run test:e2e
```

> [!TIP]
> **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.

You can also run tests interactively with the Playwright UI runner:

```shell
npx playwright test --ui
```

---

## Updating Ember.js

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`:
Expand All @@ -148,18 +191,3 @@ The client project uses standard npm packaging and is configured with Ember's mo
```shell
npm test
```

---

## Manual Verification Checklist

When verifying changes or following an upgrade, check that:

- [ ] Application loads at [http://localhost:4200](http://localhost:4200) and displays the sign-in form.
- [ ] Attempting to log in with invalid credentials displays an `"Authentication failed"` alert.
- [ ] Logging in as `guest` (`Guest1!`) displays the todo list containing `"owned-by-guest"`.
- [ ] Logging in as `john` (`P@ssw0rd!`) displays the todo list containing `"owned-by-john"`.
- [ ] Input validation: Attempting to save a todo-item with fewer than 4 characters displays a validation error.
- [ ] Adding a valid todo-item saves successfully and navigates back to the updated list.
- [ ] Clicking **Logout** invalidates the session and returns to the login screen.
- [ ] 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.
2 changes: 1 addition & 1 deletion TodoListAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
{
app.UseCors(policy =>
{
policy.WithOrigins("http://localhost:5000").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
policy.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
});
}

Expand Down
2 changes: 2 additions & 0 deletions TodoListClient/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
/npm-debug.log*
/testem.log
/yarn-error.log
/test-results/
/playwright-report/

# ember-try
/.node_modules.ember-try/
Expand Down
4 changes: 4 additions & 0 deletions TodoListClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ From the `TodoListClient` directory:
```
- Run tests interactively in the browser:
Start the dev server (`npm start`) and navigate to [http://localhost:4200/tests](http://localhost:4200/tests).
- Run end-to-end tests against the real API (requires PostgreSQL running in Docker):
```shell
npm run test:e2e
```

## Linting & Formatting

Expand Down
137 changes: 137 additions & 0 deletions TodoListClient/e2e/app.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { test, expect } from '@playwright/test';

test.describe('Todo List Application', () => {
test('Application loads at http://localhost:4200 and displays the sign-in form', async ({
page,
}) => {
await page.goto('/');
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('h1')).toHaveText('Please sign in');
await expect(page.locator('[data-test-username]')).toBeVisible();
await expect(page.locator('[data-test-password]')).toBeVisible();
await expect(page.locator('[data-test-submit]')).toBeVisible();
});

test('Attempting to log in with invalid credentials displays an "Authentication failed" alert', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('wrong-user');
await page.locator('[data-test-password]').fill('wrong-password');
await page.locator('[data-test-submit]').click();

const alertMessage = page.locator('.ember-notify .message');
await expect(alertMessage).toBeVisible();
await expect(alertMessage).toHaveText('Authentication failed');
await expect(page).toHaveURL(/\/login/);
});

test('Logging in as guest displays the todo list containing "owned-by-guest"', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('guest');
await page.locator('[data-test-password]').fill('Guest1!');
await page.locator('[data-test-submit]').click();

await expect(page).toHaveURL(/\/s\/todo-items/);
await expect(page.locator('h1')).toHaveText('Todo Items');
await expect(page.locator('[data-test-description]')).toContainText([
'owned-by-guest',
]);
await expect(page.locator('tbody')).not.toContainText('owned-by-john');
});

test('Logging in as john displays the todo list containing "owned-by-john"', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('john');
await page.locator('[data-test-password]').fill('P@ssw0rd!');
await page.locator('[data-test-submit]').click();

await expect(page).toHaveURL(/\/s\/todo-items/);
await expect(page.locator('h1')).toHaveText('Todo Items');
await expect(page.locator('[data-test-description]')).toContainText([
'owned-by-john',
]);
await expect(page.locator('tbody')).not.toContainText('owned-by-guest');
});

test('Input validation: Attempting to save a todo-item with fewer than 4 characters displays a validation error', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('guest');
await page.locator('[data-test-password]').fill('Guest1!');
await page.locator('[data-test-submit]').click();
await expect(page).toHaveURL(/\/s\/todo-items/);

await page.locator('[data-test-add]').click();
await expect(page).toHaveURL(/\/s\/todo-items\/add/);

// Empty description validation
await page.locator('[data-test-submit]').click();
let alertMessage = page.locator('.ember-notify .message');
await expect(alertMessage).toBeVisible();
await expect(alertMessage).toContainText("Description can't be blank");

// Short description (< 4 characters) validation
await page.locator('[data-test-description-input]').fill('abc');
await page.locator('[data-test-submit]').click();
alertMessage = page.locator('.ember-notify .message').last();
await expect(alertMessage).toBeVisible();
await expect(alertMessage).toContainText(
'Description is too short (minimum is 4 characters)',
);
await expect(page).toHaveURL(/\/s\/todo-items\/add/);
});

test('Adding a valid todo-item saves successfully and navigates back to the updated list', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('guest');
await page.locator('[data-test-password]').fill('Guest1!');
await page.locator('[data-test-submit]').click();
await expect(page).toHaveURL(/\/s\/todo-items/);

await page.locator('[data-test-add]').click();
await expect(page).toHaveURL(/\/s\/todo-items\/add/);

const newDescription = `valid-todo-item-${Date.now()}`;
await page.locator('[data-test-description-input]').fill(newDescription);
await page.locator('[data-test-submit]').click();

await expect(page).toHaveURL(/\/s\/todo-items/);
await expect(page.locator('[data-test-description]')).toContainText([
newDescription,
]);
});

test('Clicking Logout invalidates the session and returns to the login screen', async ({
page,
}) => {
await page.goto('/login');
await page.locator('[data-test-username]').fill('guest');
await page.locator('[data-test-password]').fill('Guest1!');
await page.locator('[data-test-submit]').click();
await expect(page).toHaveURL(/\/s\/todo-items/);

await page.locator('[data-test-logout]').click();
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('h1')).toHaveText('Please sign in');
});

test('Navigating directly to a protected route while logged out redirects to the login screen', async ({
page,
}) => {
await page.goto('/s/todo-items');
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('h1')).toHaveText('Please sign in');

await page.goto('/s/todo-items/add');
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('h1')).toHaveText('Please sign in');
});
});
46 changes: 46 additions & 0 deletions TodoListClient/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion TodoListClient/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"lint:js:fix": "eslint . --fix",
"postinstall": "patch-package",
"start": "vite",
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\" --prefixColors auto",
"test": "concurrently \"npm:lint\" \"npm:test:ember\" --names \"lint,test:\" --prefixColors auto",
"test:e2e": "playwright test",
"test:ember": "vite build --mode development && ember test --path dist"
},
"devDependencies": {
Expand All @@ -44,6 +45,7 @@
"@eslint/js": "^9.25.0",
"@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2",
"@playwright/test": "^1.63.0",
"@rollup/plugin-babel": "^7.0.0",
"@warp-drive/build-config": "^0.0.3",
"babel-plugin-ember-template-compilation": "^3.1.0",
Expand Down
Loading
Loading