diff --git a/frontend/website/cypress/e2e/confirm-email.cy.ts b/frontend/website/cypress/e2e/confirm-email.cy.ts index 2ef0274..92bdf79 100644 --- a/frontend/website/cypress/e2e/confirm-email.cy.ts +++ b/frontend/website/cypress/e2e/confirm-email.cy.ts @@ -17,6 +17,12 @@ describe('email confirmation', () => { statusCode: 200, body: { schedules: [] }, }) + cy.intercept('GET', '**/api/assignments?date=*', (request) => { + request.reply({ + statusCode: 200, + body: { date: request.query.date, assignments: [] }, + }) + }) }) it('chooses a password, confirms the account, and opens the dashboard', () => { @@ -39,7 +45,7 @@ describe('email confirmation', () => { cy.wait('@confirmEmail') cy.location('pathname').should('equal', '/dashboard') - cy.get('h1').should('have.text', 'Available sets') + cy.get('#sets-heading').should('have.text', 'Available sets') }) it('validates password length and confirmation before submitting', () => { diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts index eb88490..d4b863a 100644 --- a/frontend/website/cypress/e2e/session-auth.cy.ts +++ b/frontend/website/cypress/e2e/session-auth.cy.ts @@ -47,6 +47,12 @@ describe('session authentication', () => { statusCode: 200, body: { schedules: [] }, }) + cy.intercept('GET', '**/api/assignments?date=*', (request) => { + request.reply({ + statusCode: 200, + body: { date: request.query.date, assignments: [] }, + }) + }) }) it('restores an authenticated session on a protected route', () => { @@ -59,7 +65,7 @@ describe('session authentication', () => { cy.wait('@me') cy.location('pathname').should('equal', '/dashboard') - cy.get('h1').should('have.text', 'Available sets') + cy.get('#sets-heading').should('have.text', 'Available sets') }) it('redirects an unauthenticated protected route to login', () => { diff --git a/frontend/website/cypress/e2e/set-scheduling.cy.ts b/frontend/website/cypress/e2e/set-scheduling.cy.ts index 5747d6b..a12f672 100644 --- a/frontend/website/cypress/e2e/set-scheduling.cy.ts +++ b/frontend/website/cypress/e2e/set-scheduling.cy.ts @@ -250,6 +250,12 @@ describe('set scheduling', () => { }, }) }).as('schedules') + cy.intercept('GET', '**/api/assignments?date=*', (request) => { + request.reply({ + statusCode: 200, + body: { date: request.query.date, assignments: [] }, + }) + }) cy.visit('/dashboard') cy.wait('@me') diff --git a/frontend/website/cypress/e2e/sets-dashboard.cy.ts b/frontend/website/cypress/e2e/sets-dashboard.cy.ts index 2557ded..5c9b28e 100644 --- a/frontend/website/cypress/e2e/sets-dashboard.cy.ts +++ b/frontend/website/cypress/e2e/sets-dashboard.cy.ts @@ -17,6 +17,12 @@ describe('sets dashboard', () => { statusCode: 200, body: { schedules: [] }, }) + cy.intercept('GET', '**/api/assignments?date=*', (request) => { + request.reply({ + statusCode: 200, + body: { date: request.query.date, assignments: [] }, + }) + }) }) it('shows every available set as a detail link', () => { @@ -38,7 +44,7 @@ describe('sets dashboard', () => { cy.wait('@me') cy.wait('@sets') - cy.get('h1').should('have.text', 'Available sets') + cy.get('#sets-heading').should('have.text', 'Available sets') cy.get('ul[aria-label="Available sets"] h2').then(($headings) => { expect([...$headings].map((heading) => heading.textContent)).to.deep.equal([ 'Bible', diff --git a/frontend/website/cypress/e2e/today-assignments.cy.ts b/frontend/website/cypress/e2e/today-assignments.cy.ts new file mode 100644 index 0000000..ee94323 --- /dev/null +++ b/frontend/website/cypress/e2e/today-assignments.cy.ts @@ -0,0 +1,157 @@ +const authenticatedUser = { + id: 7, + email: 'user@example.com', +} + +const browserToday = '2026-08-15' + +function interceptDashboardRequests(): void { + cy.intercept('GET', '**/api/me', { + statusCode: 200, + body: { user: authenticatedUser }, + }).as('me') + cy.intercept('GET', '**/api/sets', { + statusCode: 200, + body: { sets: [] }, + }).as('sets') + cy.intercept('GET', '**/api/schedules', { + statusCode: 200, + body: { schedules: [] }, + }).as('schedules') +} + +describe("today's assignments", () => { + beforeEach(() => { + cy.clock(new Date(2026, 7, 15, 0, 30).getTime()) + interceptDashboardRequests() + }) + + it('uses the browser date and links every assignment to its schedule', () => { + cy.intercept('GET', `**/api/assignments?date=${browserToday}`, (request) => { + expect(request.headers.accept).to.equal('application/json') + request.reply({ + statusCode: 200, + body: { + date: browserToday, + assignments: [ + { + id: 12, + schedule: { + id: 73, + set: { name: 'Bible' }, + }, + element: { + name: 'Chapter 1', + kind: 'Chapter_sections-v2', + path: ['Genesis', 'Creation', 'Chapter 1'], + }, + }, + { + id: 13, + schedule: { + id: 81, + set: { name: 'Course' }, + }, + element: { + name: 'Introduction', + kind: 'lesson', + path: ['Introduction'], + }, + }, + ], + }, + }) + }).as('todayAssignments') + + cy.visit('/dashboard') + cy.wait('@me') + cy.wait('@todayAssignments') + + cy.get('#today-heading').should('have.text', 'Today') + cy.get('[data-today-date]').should( + 'have.attr', + 'data-today-date', + browserToday, + ) + cy.get('ul[aria-label="Today\'s assignments"] > li').should( + 'have.length', + 2, + ) + cy.contains('a', 'Genesis / Creation / Chapter 1') + .should('contain.text', 'Bible') + .and('have.attr', 'href', '/schedules/73') + cy.contains('a', 'Introduction') + .should('contain.text', 'Course') + .and('have.attr', 'href', '/schedules/81') + cy.get('.today-assignment__kind') + .first() + .should('have.text', 'Chapter_sections-v2') + .and('have.css', 'text-transform', 'none') + }) + + it('shows loading and empty states', () => { + cy.intercept('GET', `**/api/assignments?date=${browserToday}`, { + delay: 500, + statusCode: 200, + body: { + date: browserToday, + assignments: [], + }, + }).as('todayAssignments') + + cy.visit('/dashboard') + cy.wait('@me') + cy.get('.today-assignments [role="status"]').should( + 'have.text', + "Loading today's assignments...", + ) + cy.wait('@todayAssignments') + + cy.get('.today-assignments [role="status"]').should( + 'have.text', + 'Nothing is assigned for today.', + ) + }) + + it('retries independently after the request fails', () => { + let requestCount = 0 + cy.intercept( + 'GET', + `**/api/assignments?date=${browserToday}`, + (request) => { + requestCount += 1 + request.alias = `todayAssignments${requestCount}` + + if (requestCount === 1) { + request.reply({ statusCode: 500 }) + return + } + + request.reply({ + statusCode: 200, + body: { + date: browserToday, + assignments: [], + }, + }) + }, + ) + + cy.visit('/dashboard') + cy.wait('@me') + cy.wait('@todayAssignments1') + + cy.get('.today-assignments [role="alert"]') + .should('contain.text', "We couldn't load today's assignments.") + .within(() => { + cy.contains('button', 'Try again').click() + }) + cy.get('#schedules-heading').should('have.text', 'Your schedules') + cy.wait('@todayAssignments2') + + cy.get('.today-assignments [role="status"]').should( + 'have.text', + 'Nothing is assigned for today.', + ) + }) +})