Attainly/frontend/website/cypress/e2e/today-assignments.cy.ts

364 lines
11 KiB
TypeScript

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<HTMLElement>('.today-assignment')
const backgroundCard = stack.querySelector<HTMLElement>('[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.',
)
})
})