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
5 changes: 5 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ tasks:
desc: Run backend integration tests only (Testcontainers PostgreSQL, Docker required)
cmd: cd server && ./mvnw --no-transfer-progress verify -P tests -Dskip.unit.tests=true -Djacoco.skip=true -Dcheckstyle.skip=true

test-client-cypress:
desc: Run headless cypress tests on client
dir: client
cmd: npm run cypress:run:headless

run-native-tracing:
desc: Buidl and run with native agent to trace reflections
deps: [db-up]
Expand Down
23 changes: 23 additions & 0 deletions client/cypress/e2e/auth.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ describe('Authentication', () => {
body: { token: 'fake-jwt-token', email: 'user@example.com' }
}).as('signIn');

cy.intercept('GET', /\/rest\/users\/me/, {
statusCode: 200,
body: {
userId: 1,
name: 'Test User',
email: 'user@example.com',
admin: false,
createdAt: '2026-01-01T00:00:00.000Z',
gravatarImageUrl: '',
lang: 'en'
}
}).as('getCurrentUser');

cy.intercept('GET', /\/rest\/home\/tasks\/tags/, {
statusCode: 200,
body: []
}).as('getTags');

cy.intercept('GET', /\/rest\/home\/items/, {
statusCode: 200,
body: { tasks: [], notes: [] }
}).as('getItems');

cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('Password1!');
cy.get('button[type="submit"]').click();
Expand Down
78 changes: 54 additions & 24 deletions client/cypress/e2e/home.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const mockTasks = [
{
id: 1,
description: 'Buy groceries',
done: false,
completed: false,
highPriority: true,
dueDate: '2026-06-01',
dueDateFmt: 'Jun 1, 2026',
Expand All @@ -27,7 +27,7 @@ const mockTasks = [
{
id: 2,
description: 'Write report',
done: false,
completed: false,
highPriority: false,
dueDate: '',
dueDateFmt: '',
Expand All @@ -46,7 +46,8 @@ const mockNotes = [
tags: ['work'],
lastUpdate: '2026-05-01',
shared: false,
shareToken: null
shareToken: null,
archived: false
},
{
id: 2,
Expand All @@ -56,12 +57,43 @@ const mockNotes = [
tags: ['personal'],
lastUpdate: '2026-05-02',
shared: false,
shareToken: null
shareToken: null,
archived: false
}
];

const mockTags = ['personal', 'work'];

/**
* Replies to the aggregated /rest/home/items endpoint replicating the
* server-side search (q), type, and tag filtering semantics.
*
* @param {object} req - The intercepted request.
*/
const replyWithFilteredItems = (req: { url: string; reply: (response: object) => void }): void => {
const url = new URL(req.url);
const query = (url.searchParams.get('q') ?? '').toLowerCase();
const type = url.searchParams.get('type');
const tag = url.searchParams.get('tag');

let tasks = type === 'notes' ? [] : [...mockTasks];
let notes = type === 'tasks' ? [] : [...mockNotes];

if (tag) {
tasks = tasks.filter(task => task.tags.includes(tag));
notes = notes.filter(note => note.tags.includes(tag));
}

if (query) {
tasks = tasks.filter(task => task.description.toLowerCase().includes(query));
notes = notes.filter(note =>
note.title.toLowerCase().includes(query)
|| note.description.toLowerCase().includes(query));
}

req.reply({ statusCode: 200, body: { tasks, notes } });
};

describe('Home Management', () => {
beforeEach(() => {
cy.intercept('GET', /\/rest\/user-sessions\/refresh/, {
Expand All @@ -79,15 +111,7 @@ describe('Home Management', () => {
body: mockTags
}).as('getTags');

cy.intercept('GET', /\/rest\/tasks$/, {
statusCode: 200,
body: mockTasks
}).as('getTasks');

cy.intercept('GET', /\/rest\/notes$/, {
statusCode: 200,
body: mockNotes
}).as('getNotes');
cy.intercept('GET', /\/rest\/home\/items/, replyWithFilteredItems).as('getItems');

cy.visit('/home', {
onBeforeLoad(win) {
Expand All @@ -102,8 +126,7 @@ describe('Home Management', () => {
*/
describe('Display', () => {
it('shows tasks and notes after loading', () => {
cy.wait('@getTasks');
cy.wait('@getNotes');
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Write report').should('be.visible');
Expand All @@ -117,12 +140,12 @@ describe('Home Management', () => {
*/
describe('Search', () => {
beforeEach(() => {
cy.wait('@getTasks');
cy.wait('@getNotes');
cy.wait('@getItems');
});

it('filters items by search text matching a task description', () => {
cy.get('input[placeholder="Filter tasks & notes"]').type('groceries');
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Write report').should('not.exist');
Expand All @@ -132,6 +155,7 @@ describe('Home Management', () => {

it('filters items by search text matching a note title', () => {
cy.get('input[placeholder="Filter tasks & notes"]').type('Meeting');
cy.wait('@getItems');

cy.contains('Meeting notes').should('be.visible');
cy.contains('Buy groceries').should('not.exist');
Expand All @@ -140,9 +164,11 @@ describe('Home Management', () => {

it('restores all items after clearing the search', () => {
cy.get('input[placeholder="Filter tasks & notes"]').type('groceries');
cy.wait('@getItems');
cy.contains('Write report').should('not.exist');

cy.get('input[placeholder="Filter tasks & notes"]').clear();
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Meeting notes').should('be.visible');
Expand All @@ -154,13 +180,13 @@ describe('Home Management', () => {
*/
describe('Dropdown Filter', () => {
beforeEach(() => {
cy.wait('@getTasks');
cy.wait('@getNotes');
cy.wait('@getItems');
});

it('shows only tasks when Tasks filter is selected', () => {
cy.get('[data-testid="dropdown-tag-filter"]').click();
cy.contains('Tasks').click();
cy.contains('.dropdown-item', 'Tasks').click();
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Write report').should('be.visible');
Expand All @@ -170,7 +196,8 @@ describe('Home Management', () => {

it('shows only notes when Notes filter is selected', () => {
cy.get('[data-testid="dropdown-tag-filter"]').click();
cy.contains('Notes').click();
cy.contains('.dropdown-item', 'Notes').click();
cy.wait('@getItems');

cy.contains('Meeting notes').should('be.visible');
cy.contains('Recipe').should('be.visible');
Expand All @@ -180,10 +207,12 @@ describe('Home Management', () => {

it('shows everything when Everything filter is selected', () => {
cy.get('[data-testid="dropdown-tag-filter"]').click();
cy.contains('Tasks').click();
cy.contains('.dropdown-item', 'Tasks').click();
cy.wait('@getItems');

cy.get('[data-testid="dropdown-tag-filter"]').click();
cy.contains('Everything').click();
cy.contains('.dropdown-item', 'Everything').click();
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Meeting notes').should('be.visible');
Expand All @@ -193,7 +222,8 @@ describe('Home Management', () => {
cy.wait('@getTags');

cy.get('[data-testid="dropdown-tag-filter"]').click();
cy.contains('#personal').click();
cy.contains('.dropdown-item', '#personal').click();
cy.wait('@getItems');

cy.contains('Buy groceries').should('be.visible');
cy.contains('Recipe').should('be.visible');
Expand Down
Loading
Loading