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('shows only the earliest due assignment for each 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, scheduledDate: '2026-08-13', schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 1', kind: 'Chapter_sections-v2', path: ['Genesis', 'Creation', 'Chapter 1'], }, }, { id: 13, scheduledDate: browserToday, schedule: { id: 81, set: { name: 'Course' }, }, element: { name: 'Introduction', kind: 'lesson', path: ['Introduction'], }, }, { id: 14, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 2', kind: 'Chapter_sections-v2', path: ['Genesis', 'The garden', 'Chapter 2'], }, }, { id: 15, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 3', kind: 'Chapter_sections-v2', path: ['Genesis', 'The fall', 'Chapter 3'], }, }, { id: 16, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 4', kind: 'Chapter_sections-v2', path: ['Genesis', 'Cain and Abel', 'Chapter 4'], }, }, { id: 17, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 5', kind: 'Chapter_sections-v2', path: ['Genesis', 'Generations', 'Chapter 5'], }, }, ], }, }) }).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.get('[data-today-assignment]') .first() .should('have.attr', 'data-today-assignment', '12') cy.get('[data-today-assignment="12"] .today-assignment__overdue').should( 'have.text', 'Overdue ยท Due Aug 13, 2026', ) cy.get('[data-today-assignment="13"] .today-assignment__overdue').should( 'not.exist', ) cy.get('[data-today-assignment="14"]').should('not.exist') cy.get('[data-today-assignment="15"]').should('not.exist') cy.get('[data-today-assignment="16"]').should('not.exist') cy.get('[data-today-assignment="17"]').should('not.exist') cy.get('[data-today-schedule="73"]') .should('have.attr', 'data-due-count', '5') .and('contain.text', '5 due now') .and('have.class', 'today-assignment-stack--queued') .within(() => { cy.get('[data-stack-layer]').should('have.length', 3) }) .then(($stack) => { const stack = $stack[0] if (stack === undefined) { throw new Error('today assignment stack not found') } const assignmentCard = stack.querySelector('.today-assignment') const backgroundCard = stack.querySelector('[data-stack-layer]') if (assignmentCard === null || backgroundCard === null) { throw new Error('today assignment stack cards not found') } expect(backgroundCard.getBoundingClientRect().left).to.be.lessThan( assignmentCard.getBoundingClientRect().left, ) }) cy.get('[data-today-schedule="81"]') .should('have.attr', 'data-due-count', '1') .and('not.have.class', 'today-assignment-stack--queued') .within(() => { cy.get('.today-assignment__queue-count').should('not.exist') cy.get('[data-stack-layer]').should('not.exist') }) 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: 2500, statusCode: 200, body: { date: browserToday, assignments: [], }, }).as('todayAssignments') cy.visit('/dashboard') cy.wait('@me') cy.get('.today-assignments [role="status"]').should( 'contain.text', "Loading today's assignments...", ) cy.wait('@todayAssignments') cy.get('.today-assignments [role="status"]').should( 'contain.text', 'Nothing is due today.', ) }) it('completes an assignment and keeps failures retryable', () => { cy.intercept('GET', `**/api/assignments?date=${browserToday}`, { statusCode: 200, body: { date: browserToday, assignments: [ { id: 12, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 1', kind: 'chapter', path: ['Genesis', 'Chapter 1'], }, }, { id: 13, scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, }, element: { name: 'Chapter 2', kind: 'chapter', path: ['Genesis', 'Chapter 2'], }, }, ], }, }).as('todayAssignments') let requestCount = 0 cy.intercept('PATCH', '**/api/assignments/12', (request) => { requestCount += 1 request.alias = `completion${requestCount}` expect(request.body).to.deep.equal({ completed: true }) if (requestCount === 1) { request.reply({ delay: 400, statusCode: 500 }) return } request.reply({ delay: 400, statusCode: 200, body: { assignment: { id: 12, completedAt: '2026-08-15T12:30:00+00:00', }, }, }) }) cy.intercept('PATCH', '**/api/assignments/13', (request) => { expect(request.body).to.deep.equal({ completed: true }) request.reply({ delay: 400, statusCode: 200, body: { assignment: { id: 13, completedAt: '2026-08-15T12:35:00+00:00', }, }, }) }).as('nextCompletion') cy.visit('/dashboard') cy.wait('@me') cy.wait('@todayAssignments') cy.get('[data-today-assignment="12"]').within(() => { cy.get('[data-stack-layer]').should('have.length', 1) cy.contains('button', 'Mark complete').click() cy.contains('button', 'Completing...').should('be.disabled') }) cy.wait('@completion1') cy.get('[data-today-assignment="12"]') .should('contain.text', "We couldn't update this assignment.") .and('contain.text', '2 due now') .within(() => { cy.get('[data-stack-layer]').should('have.length', 1) cy.contains('button', 'Mark complete').click() cy.contains('button', 'Completing...').should('be.disabled') }) cy.wait('@completion2') cy.get('[data-today-assignment="12"]').should('not.exist') cy.get('[data-today-assignment="13"]') .should('be.visible') .and('have.attr', 'data-due-count', '1') .within(() => { cy.get('.today-assignment__queue-count').should('not.exist') cy.get('[data-stack-layer]').should('not.exist') cy.contains('button', 'Mark complete').click() }) cy.wait('@nextCompletion') cy.get('[data-today-assignment="13"]').should('not.exist') cy.get('.today-assignments [role="status"]').should( 'contain.text', 'Nothing is due 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( 'contain.text', 'Nothing is due today.', ) }) })